socket通信(二)
分包、粘包
TCP虽然可以保证顺序不变,但是依然可能会发生分包和粘包的问题:
-
分包:发送一个包”helloworld“,但对方却收到了两个包”hello“和”world“
-
粘包:和分包相反
recv()函数参数需要指定接收的字节长度,但是实际开发中是不知道对方发的消息内容有多少个字节,所以一般就是设置为buffer的长度,这就会导致出现粘包和分包的问题
解决方法:自定义一份协议规定 报文长度+报文内容:1010helloworld。报文长度采用ascii码(十进制)或者二进制的数字;接收时,可以先接收前四位看长度,然后再recv后面的报文内容。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
len不对会导致粘包、分包
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
写的时候也可能写不完(缓冲区不足),就需要循环多次使用
多进程socket

int main()
{
// signal(SIGCHLD,SIG_IGN); // 忽略子进程退出的信号,避免产生僵尸进程
if (TcpServer.InitServer(5051)==false)
{ printf("服务端初始化失败,程序退出。\n"); return -1; }
while (1)
{
if (TcpServer.Accept() == false) continue;
if (fork()>0) { TcpServer.CloseClient(); continue; } // 父进程回到while,继续Accept。
// 子进程负责与客户端进行通信,直到客户端断开连接。
TcpServer.CloseListen();
printf("客户端已连接。\n");
// 与客户端通信,接收客户端发过来的报文后,回复ok。
char strbuffer[1024];
while (1)
{
memset(strbuffer,0,sizeof(strbuffer));
if (TcpServer.Recv(strbuffer,sizeof(strbuffer))<=0) break;
printf("接收:%s\n",strbuffer);
strcpy(strbuffer,"ok");
if (TcpServer.Send(strbuffer,strlen(strbuffer))<=0) break;
printf("发送:%s\n",strbuffer);
}
printf("客户端已断开连接。\n");
return 0; // 或者exit(0),子进程退出。
}
}
- fork返回值大于0,说明处于父进程,父进程就继续监听
- fork返回值为0,说明处于子进程,子进程处理连接到socket
tips:这里用了一些freecplus的框架,封装了原始的socket相关函数,使用比较方便,大致的用途看函数名应该也能看出来
僵尸进程:子进程相比于父进程提前结束,而父进程也没有释放子进程的资源,就会导致僵尸进程,导致持续占用资源。使用signal(SIGCHLD,SIG_IGN)
屏蔽子进程退出的信号,就可以避免僵尸进程。
上面的代码主要内容就是父进程持续监听,监听到了连接就开辟子进程处理。因为使用fork会导致子进程完全复制父进程的资源,这就导致了一些浪费:父进程不需要clientfd(因为监听到了之后都由子进程处理,父进程只负责监听),而子进程又不需要listenfd(因为监听是父进程的任务)。所以这就是以下代码中close的原因。
if (fork()>0) { TcpServer.CloseClient(); continue; } // 父进程回到while,继续Accept。
// 子进程负责与客户端进行通信,直到客户端断开连接。
TcpServer.CloseListen();
IO复用
unix中,一切皆文件,socket也是文件,信息交换就是对这些流进行数据收发操作,即IO操作。
前面介绍的是多进程的网络服务器,这在高并发时并不适用。引进了IO复用:使用单个进程/线程就可以管理多个socket(上百万个)
IO复用的方案有三种,各有优缺点:
- select(早期采用)
- poll
- epoll
select
select流程如下图:

- 监听的socket用于等待客户端的连接,客户端的socket用于等待客户端的报文。
- 在多进程方法中,监听socket会阻塞在accept(),客户端socket会阻塞在recv();但是IO复用中,这两类socket都会阻塞在select()中。
- 如果socket集合中触发了事件(比如新的连接,数据传输或者连接断开),select就会返回有事件的socket。
整体代码如下:
while (1)
{
// 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
fd_set tmpfdset = readfdset;
int infds = select(maxfd+1,&tmpfdset,NULL,NULL,NULL);
// printf("select infds=%d\n",infds);
// 返回失败。
if (infds < 0)
{
printf("select() failed.\n"); perror("select()"); break;
}
// 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。
if (infds == 0)
{
printf("select() timeout.\n"); continue;
}
// 检查有事情发生的socket,包括监听和客户端连接的socket。
// 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
for (int eventfd=0; eventfd <= maxfd; eventfd++)
{
// 询问这个socket是否有事件,没有则continue
if (FD_ISSET(eventfd,&tmpfdset)<=0) continue;
// 发生事件,且是listensocket发生事件
if (eventfd==listensock)
{
// 如果发生事件的是listensock,表示有新的客户端连上来。
// 创建新的client socket
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0)
{
printf("accept() failed.\n"); continue;
}
printf ("client(socket=%d) connected ok.\n",clientsock);
// 把新的客户端socket加入集合。
// 加入的是原集合,而不是tmpfdset
FD_SET(clientsock,&readfdset);
if (maxfd < clientsock) maxfd = clientsock;
continue;
}
// 客户端socket发生事件
else
{
// 客户端有数据过来或客户端的socket连接被断开。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
// 读取客户端的数据。
ssize_t isize=read(eventfd,buffer,sizeof(buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket。
FD_CLR(eventfd,&readfdset); // 从集合中移去客户端的socket。
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd;ii>0;ii--)
{
if (FD_ISSET(ii,&readfdset))
{
maxfd = ii; break;
}
}
printf("maxfd=%d\n",maxfd);
}
continue;
}
printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer);
// 把收到的报文发回给客户端。
write(eventfd,buffer,strlen(buffer));
}
}
}
可以分为三个部分:
-
创建socket集合副本,并通过select阻塞,有事件发生时再触发:
// 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。 fd_set tmpfdset = readfdset; int infds = select(maxfd+1,&tmpfdset,NULL,NULL,NULL); // printf("select infds=%d\n",infds); // 返回失败。 if (infds < 0) { printf("select() failed.\n"); perror("select()"); break; } // 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。 if (infds == 0) { printf("select() timeout.\n"); continue; }
返回值infds分为正值或负值或0,负值和0在上面说明了,正值表示某些文件可读写(事件触发),所以之后接一个循环扫描FD_SET处理事件。
-
处理连接事件
// 询问这个socket是否有事件,没有则continue if (FD_ISSET(eventfd,&tmpfdset)<=0) continue; // 发生事件,且是listensocket发生事件 if (eventfd==listensock) { // 如果发生事件的是listensock,表示有新的客户端连上来。 // 创建新的client socket struct sockaddr_in client; socklen_t len = sizeof(client); int clientsock = accept(listensock,(struct sockaddr*)&client,&len); if (clientsock < 0) { printf("accept() failed.\n"); continue; } printf ("client(socket=%d) connected ok.\n",clientsock); // 把新的客户端socket加入集合。 // 加入的是原集合,而不是tmpfdset FD_SET(clientsock,&readfdset); if (maxfd < clientsock) maxfd = clientsock; continue; }
-
处理数据写入事件和断开连接事件
else { // 客户端有数据过来或客户端的socket连接被断开。 char buffer[1024]; memset(buffer,0,sizeof(buffer)); // 读取客户端的数据。 ssize_t isize=read(eventfd,buffer,sizeof(buffer)); // 发生了错误或socket被对方关闭。 if (isize <=0) { printf("client(eventfd=%d) disconnected.\n",eventfd); close(eventfd); // 关闭客户端的socket。 FD_CLR(eventfd,&readfdset); // 从集合中移去客户端的socket。 // 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。 if (eventfd == maxfd) { for (int ii=maxfd;ii>0;ii--) { if (FD_ISSET(ii,&readfdset)) { maxfd = ii; break; } } printf("maxfd=%d\n",maxfd); } continue; } printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer); // 把收到的报文发回给客户端。 write(eventfd,buffer,strlen(buffer)); }
fdset默认设置大小为1024,相当于一个位图,哪里存放了fd,就将哪里置为1。所以select默认支持1024个socket的集合。可以调整,但一般不修改。
/* Number of descriptors that can fit in an `fd_set'. */
#define __FD_SETSIZE 1024
一共有四个宏:
// 将fd位置置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd是否在集合中,是则为1,否为0
int FD_ISSET(int fd, fd_set *set);
// 将fd位置置为1
void FD_SET(int fd, fd_set *set);
// 将位图全部置为0
void FD_ZERO(fd_set *set);
select监视到位图中某个fd事件触发,就会把位图中其它fd都置为零,相当于清空其它未触发事件的fd,所以这是为什么可以用FD_ISSET检查事件触发的原因,也是为什么使用副本的原因。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- nfds:fd集合个数,最大序号+1(maxfd+1)
- readfds:读fd集合
- writefds:写fd集合,因为写很少阻塞,一般不怎么用
- exceptfds:很少用
- timeout:超时机制,select阻塞等待的时间
返回值:成功的话,返回事件fd的个数(参数里面的三个fd集合都可能触发事件返回,只不过一般只用到了readfds)。
水平触发:如果数据一次没有被处理完,会立即触发事件再继续处理数据。
缺点:
- 支持的fd集合太小了,虽然可以调整,但集合越大效率越低,详见后面两点
- 每次调用select,都需要把fdset从用户态拷贝到内核态
- 每次想要找到事件触发的fd,都要遍历整个fd集合
poll
和select没有本质上的区别,多个fd也是采用轮询管理,主要区别如下:
- poll没有最大fd数量的限制。
- poll采用数组管理fdset
poll函数:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
pollfd结构体:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
初始化fdset,maxfd是自己设置的最大fd,MAXFDS是整个fds数组的容量,fd为负数则会被忽略。
int maxfd; // fds数组中需要监视的socket的大小。
struct pollfd fds[MAXNFDS]; // fds存放需要监视的socket。
for (int ii=0;ii<MAXNFDS;ii++) fds[ii].fd=-1; // 初始化数组,把全部的fd设置为-1。
初始化监听socket。
// 把listensock添加到数组中。
fds[listensock].fd=listensock;
fds[listensock].events=POLLIN; // 有数据可读事件,包括新客户端的连接、客户端socket有数据可读和客户端socket断开三种情况。
maxfd=listensock;
events和revents可以被设置为POLLIN、POLLOUT等等
使用方法和select一样,不过不需要事先备份,因为poll返回修改的是revent:
int infds = poll(fds,maxfd+1,5000);
for (int eventfd=0; eventfd <= maxfd; eventfd++)
{
if (fds[eventfd].fd<0) continue;
if ((fds[eventfd].revents&POLLIN)==0) continue;
fds[eventfd].revents=0; // 先把revents清空。
if (eventfd==listensock)
{
// 如果发生事件的是listensock,表示有新的客户端连上来。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0)
{
printf("accept() failed.\n"); continue;
}
printf ("client(socket=%d) connected ok.\n",clientsock);
if (clientsock>MAXNFDS)
{
printf("clientsock(%d)>MAXNFDS(%d)\n",clientsock,MAXNFDS); close(clientsock); continue;
}
fds[clientsock].fd=clientsock;
fds[clientsock].events=POLLIN;
fds[clientsock].revents=0;
if (maxfd < clientsock) maxfd = clientsock;
printf("maxfd=%d\n",maxfd);
continue;
}
else
{
// 客户端有数据过来或客户端的socket连接被断开。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
// 读取客户端的数据。
ssize_t isize=read(eventfd,buffer,sizeof(buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket。
fds[eventfd].fd=-1;
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd;ii>0;ii--)
{
if ( fds[ii].fd != -1)
{
maxfd = ii; break;
}
}
printf("maxfd=%d\n",maxfd);
}
continue;
}
printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer);
// 把收到的报文发回给客户端。
write(eventfd,buffer,strlen(buffer));
}
}
epoll
解决了前面两者的所有问题:fdset用户态和内核态间的拷贝以及轮询开销,采用了最合理的解决方案。
-
创建epoll的引用,本身就是一个文件描述符
epollfd = epoll_create(int size);
linux2.6.8之后这个值就被忽略了,但是必须要给一个大于0的值。
-
注册需要监视的fd和事件
// 添加监听描述符事件 struct epoll_event ev; ev.data.fd = listensock; ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev); /* EPOLL_CTL_ADD Add fd to the interest list and associate the settings specified in event with the internal file linked to fd. EPOLL_CTL_MOD Change the settings associated with fd in the interest list to the new settings specified in event. EPOLL_CTL_DEL Remove (deregister) the target file descriptor fd from the interest list. The event argument is ignored and can be NULL (but see BUGS below). */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
需要监视某个socket,就只需要通过epoll_ctl添加进去即可(设置第二个参数为EPOLL_CTL_ADD),同理移除使用EPOLL_CTL_DEL,如果修改监视的event则使用EPOLL_CTL_MOD
-
等待事件发生
struct epoll_event events[MAXEVENTS]; // 存放有事件发生的结构数组。 // 等待监视的socket有事件发生。 int infds = epoll_wait(epollfd,events,MAXEVENTS,-1);
如果有事件发生了,就会把事件写入events,MAXEVENTS表示一次返回多少个事件(也就是infds的值);最后一个参数设置超时时间。
epoll_event结构体:
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll和前面二者主要区别代码如下,epoll可以只遍历有事件发生的数组:
// 遍历有事件发生的结构数组。
for (int ii=0;ii<infds;ii++)
{
if ((events[ii].data.fd == listensock) &&(events[ii].events & EPOLLIN))
{
// 如果发生事件的是listensock,表示有新的客户端连上来。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0)
{
printf("accept() failed.\n"); continue;
}
// 把新的客户端添加到epoll中。
memset(&ev,0,sizeof(struct epoll_event));
ev.data.fd = clientsock;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
printf ("client(socket=%d) connected ok.\n",clientsock);
continue;
}
else if (events[ii].events & EPOLLIN)
{
// 客户端有数据过来或客户端的socket连接被断开。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
// 读取客户端的数据。
ssize_t isize=read(events[ii].data.fd,buffer,sizeof(buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\n",events[ii].data.fd);
// 把已断开的客户端从epoll中删除。
memset(&ev,0,sizeof(struct epoll_event));
ev.events = EPOLLIN;
ev.data.fd = events[ii].data.fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,events[ii].data.fd,&ev);
close(events[ii].data.fd);
continue;
}
printf("recv(eventfd=%d,size=%d):%s\n",events[ii].data.fd,isize,buffer);
// 把收到的报文发回给客户端。
write(events[ii].data.fd,buffer,strlen(buffer));
}
}