之前写过UDP的socket通讯,下面将介绍TCP的socket通讯,同时提供了TCP客户端与并发服务器端的几个案例。
目录
简介
socket开发API介绍
TCP并发服务器
与UDP的区别
TCP |
UDP |
|
是否面向连接 |
✓ |
X |
是否可靠 |
✓ |
X |
支持广播、多播 |
X |
✓ |
效率 |
低 |
高 |
客户端与服务器TCP通讯流程
1、创建tcp套接字socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if ( sockfd < 0 )
{
perror("socket");
exit(-1);
}
开发TCP客户端需要满足以下几点:
1、知道服务器的ip、port;
2、需主动连接服务器;
3、使用函数:socket()、connect()、send()、recv()、close()
Note:tcp的一些函数没有目的指向,比如是 send() 而不是 sendto(),这是因为 tcp 客户端与服务器通讯之前是先建立连接后通讯,使用该函数时,已经知晓目的地址。
2、connect连接函数
//主动与服务器建立连接
#include
int connect(
int sockfd;
const struct sockaddr *addr,
socklen_t len
);
参数介绍
sockfd : socket
addr : 连接的服务器地址结构
len : 地址结构体长度
Return
成功返回0
Note
connect建立连接后不会产生新的套接字
成功建立连接后才可以传输数据
3、send数据发送函数
//发送数据
#include
ssize_t send(
int sockfd,
const void * buf,
size_t nbtyes,
int flags
);
参数
sockfd :socket
buf :发送的数据
nbytes :发送缓存数据大小
flags :套接字标志(通常为0)
Return
成功发送的数据字节数
4、recv函数
//接收网络数据,阻塞式
#include
ssize_t recv(
int sockfd,
void *buf,
size_t nbytes,
int flags
);
参数
sockfd :socket
buf :接收网络数据的缓冲区
nbytes :接收缓冲区的大小(以字节为单位)
flags :套接字标志(通常为0)
Return
成功接收的字节数
客户端发送与接受数据
以下代码将发送一次数据和接受数据,服务器可使用 NetAssist(windows)、linux下可使用自己开发的
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
//create tcp socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd < 0)
{
printf("CREATE SOCKET ERROR\n");
return 0;
}
//bind是可选的, 固定ip和port
struct sockaddr_in client;
bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(8000);
client.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,,(struct sockaddr *)&client, sizeof(client));
//connect 连接服务器
struct sockaddr_in ser_addr;
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_port = htons(8080);
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = inet_addr("192.168.31.96");
connect(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr));
//给服务器发送数据
char buf[128] = "";
printf("please enter the msg to send\n");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf) - 1] = 0;//消除回车的影响
send(sockfd, buf, strlen(buf), 0);
//接收数据
char msg[128] = "";
recv(sockfd, msg, sizeof(msg), 0);
printf("received msg is %s\n",msg);
close(sockfd);
return 0;
}
开发服务器设计需要满足以下两点:
1、确定的地址和端口
2、监听(listen)与接收(accept)
设计流程:
1、socket()(主动)
2、bind() 固定ip和固定端口
3、listen()(主动变被动、创建连接队列)
4、accept() (从连接队列中提取成功建立的连接,接下来可以正式通讯)
根据上图TCP通讯流程图可以看出,服务器绑定固定端口,监听客户端的请求。客户端与服务器通讯前,要先与服务器端成功建立连接,需要经过三次握手。成功建立的连接会放入“完成链接”,服务器将使用accept()函数从该队列中取出客户端请求并处理。
1、listen()函数
//将套接字由主动改为被动
//操作系统为该套接字设置一个队列,用来记录所有链接到该套接字的连接
int listen(
int sockfd,
int backlog
);
参数
sockfd :socket监听套接字
backlog :连接队列的长度
Return
成功返回 0
2、accept()函数
//从已建立队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待
#include
int accept(
int sockfd,
struct sockaddr *cliaddr,
socklen_t *addrlen
);
参数
sockfd :socket
cliaddr :用于存放客户端socket的地址结构
addrlen:套接字地址结构体长度的地址
Return
已连接的socket
TCP应用层无法发送长度为0的数据报,因为为0表示这通讯结束。
案例代码
可以上述client联合使用,客户端连接服务器的端口要与服务器端绑定的端口一致。
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
//1、创建一个tcp监听套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2、使用bind函数给监听套接字绑定固定的ip和端口
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(8080);
server.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd,(struct sockaddr *)&server, sizeof(server));
//3、使用listen函数连接队列(主动变被动)
listen(sockfd, 10);
//4、使用accept函数从连接队列中提取已经完成的连接
struct sockaddr_in client;
bzero(&client,sizeof(client));
socklen_t len = sizeof(client);
//fd代表index客户端连接,client存储客户端的信息
int fd = accept(sockfd, (struct sockaddr *)&client, &len);
char ip[16] = "";
inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);
printf("client %s connected\n",ip);
//5、获取客户端的请求,以及回应客户端
char buf[128] = "";
recv(fd, buf, sizeof(buf), 0);
printf("client request is %s\n",buf);
send(fd,"recv", strlen("recv"), 0);
//6、关闭已连接套接字
close(fd);
//7、关闭监听套接字
close(sockfd);
return 0;
}
可以同时服务多个客户端。TCP并发服务器本质还是TCP服务器,同时其又可以服务多个客户端。以下将提供两种方法:多进程与多线程
多线程
客户端正常退出不会有影响,但如果服务器端意外退出,再次运行bind的时候需要防止端口被占用(5~6分钟)。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void *deal_client(void *sockfd);
int main(void)
{
//1、创建TCP监听套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("CREATE SOCKET!!");
}
//端口复用
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
//2、给socket绑定ip和端口信息
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8088);
server.sin_addr.s_addr = htonl(INADDR_ANY);
int result = bind(sockfd, (struct sockaddr *)&server, sizeof(server));
if (result == -1)
{
perror("bind");
}
// 3、调用listen,将sockfd调整主动变为被动
listen(sockfd, 10);
//4、提取建立完成的链接
//accept调用一次只能接待一个客户端,需要反复调用accept
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_fd = accept(sockfd, (struct sockaddr *)&client, &len);
//遍历客户端信息
char ip[16] = "";
unsigned short port = ntohs(client.sin_port);
inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);
printf("client %s is connected %hu port\n", ip, port);
//为每个客户端创建线程,单独的服务客户端
pthread_t tid;
pthread_create(&tid, NULL, deal_client, (void *)&new_fd);
//线程分离
pthread_detach(tid);
}
close(sockfd);
return 0;
}
//TCP并发(并发服务器核心代码都不相同)
void *deal_client(void *sockfd)
{
//通过arg获得已连接socket
//给服务器发啥就回啥
int fd = *(int *)sockfd;
//服务器核心代码,处理客户端请求、
while (1)
{
char data[128] = "";
int len = recv(fd, data, sizeof(data), 0);
if (!len){
break;
}
send(fd, data, len, 0);
}
close(fd);
}
多进程
父子进程、资源独立、某个进程结束不会影响已有进程,服务器更加稳定,代价是多进程会浪费资源
#include
#include
#include
#include
#include
#include
#include
#include
#include
void deal_client(int sockfd);
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket");
}
int yes;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(8088);
bind(sockfd, (struct sockaddr *)&server, sizeof(server));
listen(sockfd, 10);
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock_new = accept(sockfd, (struct sockaddr *)&client, &len);
unsigned short port = ntohs(client.sin_port);
char ip[16] = "";
inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);
printf("client ip %s connected at port %hu\n",ip,port);
//子进程
//使用fork后,会复制父进程的全部状态资源,注意下面四个关闭socket,描述符
if (fork() == 0)
{
//关闭监听socket(进程中的,以防止与父进程冲突)
close(sockfd);
//服务于客户端的核心代码
deal_client(sock_new);
//关闭已连接socket(进程中的,以防止与父进程冲突)
close(sock_new);
_exit(-1);
}
else //父进程
{
//关闭父进程中的已连接socket
close(sock_new);
}
}
//关闭父进程中的监听socket
close(sockfd);
return 0;
}
void deal_client(int sockfd)
{
while (1)
{
char buf[128] = "";
int len = recv(sockfd, buf, sizeof(buf), 0);
if (!len)
{
break;
}
//printf("the msg is %s\n",buf);
send(sockfd, buf, len, 0);
}
}