一、io多路复用
在现有模型中,似乎每一个线程都做了同样的事情,1、监听客户端消息;2、业务消息处理。
“一消息一线程”的缺点究其根本,在于让每个线程都做了同样重复、且消耗资源巨大的事情——单独持有fd、监听客户端消息。
能不能不让每个线程独占客户端fd,又把监听的工作抛出去呢?
如果有一个观察者,能感知到就绪的客户端消息,消息就绪后再通过回调,来触发线程处理对应的业务消息,就能避免用户态持有大量线程在cpu里空转。
由此,我们可以引入一种新的设计模式————【状态-观察者】:由观察者统一去监听客户端的消息,当数据已经就绪(从内核缓冲区/空间拷贝到用户缓冲区/空间),我们直接在用户空间找到就绪的fd,只让任务线程来处理就绪的IO事件,而没有消息时,负责事件处理的线程就不去占用cpu资源了。
这个“观察者”,其实就是IO多路复用器:
通过IO多路复用模型,我们从理论上解决了“一消息一线程”下【事件监听-事件处理】的耦合,解放了内存/计算资源,不用再为每个客户端的连接对应创建独立的线程,而是让监听由更高效的内核态来完成,事件处理则只交给用户态的任务处理线程,这样不仅压榨了硬件资源,而且规避了频繁的线程切换,可以做到用单线程承载多客户端的IO事件,这就是分布式并发系统的基石。
二、select
2.1 理解select模型
- 创建fd_set:它本质是一个位图(位数组),用来存放每一个客户端与服务端建立的文件描述符;
- 系统调用select:监听fd_set是否存在就绪fd的系统调用,当没有就绪的fd时,用户空间阻塞,此时用户空间不占用计算资源,但在内核中会持续循环,直到出现就绪的fd或超时返回;
- 系统调用read:网卡直接将收到客户端消息的fd拷贝到内核空间,此时内核判断出现了就绪的fd,遍历并标记对应fd,完成标记后由read系统调用将fd_set整体从内核空间拷贝回用户空间;
- 用户空间接收到select返回的fd_set,再次遍历找到被内核标记为就绪的fd,仅分配线程处理就绪fd的IO事件。
2.2 如何操作fd_set
系统提供一组宏来操作fd_set:
- D_ZERO(set):清空集合(所有位设为0)
- FD_SET(fd, set):将文件描述符fd加入集合(对应位设为1)
- FD_CLR(fd, set):将文件描述符fd从集合移除(对应位设为0)
- FD_ISSET(fd, set):检查fd是否在集合中(检查对应位是否为1,为1则可以进行recv、send操作)
我们以FD_SET为例,假设现在已经创建好了一个完整的fd_set,socket通知过来有新的客户端66号连接,我们要调用FD_SET把66号fd加入到监听列表中,我们看一下基于fd_set的数据结构,如何添加新的文件描述符到fd_set中:
2.3 “一消息一线程”模型优化为select实现
/*** 5、select方式实现多路io复用* 定义文件描述符集合,readfds 用于存储所有需要监听的文件描述符* readset 作为 readfds 的副本,因为 select 系统调用会覆盖传入的readfds*/fd_set readfds, readset;FD_ZERO(&readfds);FD_SET(sockfd, &readfds);int maxfd = sockfd;while (LOOP)// 循环,持续监听文件描述符集合中的事件{readset = readfds;// 将 readfds 集合复制到 readset 集合,因为 select 会修改传入的集合/*** select 系统调用用于监听文件描述符集合中的事件* maxfd+1:表示文件描述符集合中的最大文件描述符加1,用于指定监听的范围* &readset:指向 fd_set 结构体的指针,用于指定要监听的文件描述符集合* NULL:不监听可写事件* NULL:不监听异常事件* NULL:指向 timeval 结构体的指针,用于指定超时时间, 为 NULL 时表示无限等待* 返回值:成功时返回就绪文件描述符的数量,失败时返回-1*/int nready = select(maxfd+1, &readset, NULL, NULL, NULL);// 检查是否有错误发生if (nready == ERROR_CODE) {printf("select failed : %s\n ",strerror(errno));continue;}// 检查监听套接字是否有新的客户端连接请求if (FD_ISSET(sockfd, &readset)) {// accept// 接受新的客户端连接,返回新的客户端套接字文件描述符int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : %d\n", clientfd);// 将新的客户端套接字文件描述符添加到 readfds 集合中,用于监听其数据可读事件FD_SET(clientfd, &readfds);// 更新最大的文件描述符maxfd = (clientfd > maxfd) ? (clientfd) : (maxfd);}// 遍历从监听套接字之后的所有文件描述符,直到最大文件描述符,检查是否有数据可读for (int i = sockfd + 1; i <= maxfd; ++i) {// recv// 检查当前文件描述符i是否在就绪集合中(有数据可读)if (FD_ISSET(i, &readset)) {char buffer[BUFFER_SIZE] = {0};/*** 6、recv系统调用,接收客户端数据* clientfd:要接收数据的客户端套接字文件描述符* buf:指向接收数据的缓冲区的指针* BUFFER_SIZE:要接收的最大字节数* MSG_SIGNAL:标志位,指定接收行为* 返回值:成功时返回实际接收的字节数,若返回0表示连接已关闭,若返回-1表示出错*/int count = recv(i, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd %d disconnect!\n", i);close(i); // 关闭客户端套接字文件描述符FD_CLR(i, &readfds);// 从监控集合移除客户端fdcontinue;} else {printf("clientfd %d recv : %s\n", i, buffer);}/*** 7、发送数据给客户端* clientfd:要发送数据的客户端套接字文件描述符* buf:指向要发送数据的缓冲区的指针* recvCount:要发送的字节数* MSG_SIGNAL:标志位,指定发送行为* 返回值:成功时返回实际发送的字节数,若返回-1表示出错*/count = send(i, buffer, count, ZERO_INIT);if (count == NO_MESSAGE_SIZE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), i);close(i);// 关闭客户端套接字文件描述符FD_CLR(i, &readfds);// 从监控集合移除客户端fdcontinue;} else {printf("send message: %s, count = %d, clientfd = %d\n", buffer, count, i);}}} }
优点:做到了一个线程处理多个客户端连接,规避了“一线程一客户端”下频繁切换线程的开销。
缺点:性能瓶颈
- fd数量限制:fd_set固定大小为FD_SETSIZE位(通常为1024),无法支撑更大数量级的连接;
- O(n)遍历开销:内核与用户空间都需要通过遍历来监听所有的fd,当连接数增加时,cpu占用也会对应上升;
- 重复内存拷贝:每次调用select都需要将fd_set整体从用户拷贝到内存空间,出现就绪fd后又整体拷回。
三、poll
3.1 理解poll模型
poll模型在流程上整体框架,与select是相同的。
使用层面的区别在于:select使用了固定大小的fd_set集合去监控每一个客户端连接的事件,而poll使用的是pollfd结构体数组,突破了fd_set固定1024个fd的数量上限。
struct pollfd {int fd; /* 要监视的文件描述符 */short events; /* 应用程序关注的事件集合 (输入参数) */short revents; /* 实际发生的事件集合 (输出参数) */
};
struct pollfd fds[16384] = {0};
3.2 如何操作/使用pollfd
根据上面提到的pollfd结构体,所有的操作都要围绕fd对应的events和revents来展开,系统为我们提供了如下可配置和触发的事件,常用如下:
- 可读事件,POLLIN:0x001,有普通数据可读,表示服务端可以recv
- 可写事件,POLLOUT:0x008,表示文件描述符已经准备好接收数据,表示服务端可以send
- 错误/断开事件:POLLERR:0x008,发生错误(如socket连接被重置,设备错误);POLLHUP:0x010,连接挂断(如客户端主动断开连接);POLLNVAL:0x020,无效文件描述符(fd未打开或非法)。
3.3 select模型优化为poll实现
将select代码中使用fd_set以及对应的操作删除,改为使用poll和操作pollfd结构体数组来处理IO事件。
/*** 5、poll方式实现多路io复用*/struct pollfd fds[POLLFD_CONNECT_COUNT] = {0};fds[sockfd].fd = sockfd;// 监听套接字fds[sockfd].events = POLLIN;// 监听可读事件int maxfd = sockfd;while (LOOP) {/*** poll 系统调用用于监听文件描述符集合中的事件* maxfd+1:表示文件描述符集合中的最大文件描述符加1,用于指定监听的范围* fds:指向 pollfd 结构体数组的指针,用于指定要监听的文件描述符集合* maxfd+1:数组中元素的数量,即要监听的文件描述符数量* -1:超时时间,单位为毫秒,-1 表示无限等待* 返回值:成功时返回就绪文件描述符的数量,失败时返回-1*/int nready = poll(fds, maxfd + 1, -1);if (nready == ERROR_CODE) {// poll 失败printf("poll failed : %s\n ",strerror(errno));continue;}if (fds[sockfd].revents & POLLIN) {// 监听套接字可读事件就绪 有新的客户端连接int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : %d\n", clientfd);fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;maxfd = (clientfd > maxfd) ? (clientfd) : (maxfd);}for (int i = sockfd + 1; i <= maxfd; ++i) {if (fds[i].revents & POLLIN) {// 客户端可读事件就绪char buffer[BUFFER_SIZE] = {0};int count = recv(i, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd %d disconnect!\n", i);close(i);// 关闭客户端套接字文件描述符fds[i].fd = FD_INVAILD_CODE;// fd置为失效fds[i].events = ZERO_INIT;// 事件类型初始化continue;}printf("clientfd %d recv : %s\n", i, buffer);count = send(i, buffer, count, MSG_SIGNAL);if (count == ERROR_CODE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), i);close(i);// 关闭客户端套接字文件描述符fds[i].fd = FD_INVAILD_CODE;// fd置为失效fds[i].events = ZERO_INIT;// 事件类型初始化continue;}printf("clientfd %d send : %s\n", i, buffer);}}}
优点:
- 突破位图数量上限的优点,poll可轻松支持数万连接,客户端连接数仅受限于硬件资源
- 更合理的事件管理机制:poll只需要拷贝pollfd结构体数组,事件与结果分离(events/revents),不需要关注多个fd集合,只需要关注数组中事件就绪的客户端fd。
缺点:
- 无法规避pollfd结构体数组整体的内存拷贝
- 没有解决用户空间需要O(n)遍历才能找到需要处理io事件的fd
四、epoll
4.1 理解epoll模型
epoll为用户提供了三个系统调用,分别是:
(1)epoll_create:创建epoll实例eventpoll,初始化红黑树(rbr)和就绪链表(rdlist),返回关联的文件描述符;
// epoll_create1(0),是epoll_create的升级版,移除了size参数
int epfd = epoll_create(EPOLLFD_CONNECT_INIT);// 需要监控的连接数
(2)epoll_ctl:管理事件监听,向epoll实例添加、修改或删除监控的文件描述符;
// 事件配置结构体
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);//epoll实例,动作,文件描述符,事件结构体指针
(3)epoll_wait:等待事件就绪,阻塞等待监控的文件描述符,直到发生事件或超时。
struct epoll_event events[EPOLL_EVENT_INIT] = {0};
//epoll实例,用于接收就绪事件的事件结构体数组,数组中元素的数量,超时时间:单位为毫秒,-1表示无限等待
int nready = epoll_wait(epfd, events, EPOLL_EVENT_INIT, EPOLL_EVENT_TIMEOUT);
- 异步唤醒和跨缓冲区拷贝————规避2次O(n)查询和1次完整fd集合拷贝。
- 使用红黑树监控和管理fd集合————降低查询、添加、删除fd的时间复杂度至O(logN)。
4.2 常用的epoll_event事件类型
EPOLLIN:数据可读(从socket缓冲区recv)
EPOLLOUT:数据可写(向socket缓冲区send)
EPOLLERR:错误信息(自动监听)
EPOLLHUP:挂起(自动监听)
EPOLLET:边缘触发模式(默认水平触发LT)
4.3 优化为epoll实现io多路复用
/*** 5、epoll方式实现多路io复用* epoll_create 系统调用 创建一个 epoll 实例* EPOLLFD_CONNECT_INIT:初始化的连接数 1* 返回值:成功时返回一个非负整数,即 epoll 实例的文件描述符*/int epfd = epoll_create(EPOLLFD_CONNECT_INIT);struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;/*** epoll_ctl 系统调用用于添加、修改或删除文件描述符到 epoll 实例中* epfd:要操作的 epoll 实例的文件描述符* EPOLL_CTL_ADD:操作类型,指定添加文件描述符到 epoll 实例中* sockfd:要添加的文件描述符* &ev:指向 epoll_event 结构体的指针,关联fd与事件类型* 返回值:成功时返回0,失败时返回-1*/epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while (LOOP){struct epoll_event events[EPOLL_EVENT_INIT] = {0};/*** epoll_wait 系统调用用于等待事件发生* epfd:要等待的 epoll 实例的文件描述符* events:指向 epoll_event 结构体数组的指针,用于接收就绪事件,内核返回复写后的就绪fd* EPOLL_EVENT_INIT:数组中元素的数量,即要接收的就绪事件数量* EPOLL_EVENT_TIMEOUT:超时时间,单位为毫秒,-1 表示无限等待* 返回值:成功时返回就绪事件的数量,失败时返回-1*/int nready = epoll_wait(epfd, events, EPOLL_EVENT_INIT, EPOLL_EVENT_TIMEOUT);printf("epoll wait...\n");for (int i = 0; i < nready; ++i) {int connfd = events[i].data.fd;if (connfd == sockfd) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : [%d]\n", clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else if (events[i].events & EPOLLIN) {char buffer[BUFFER_SIZE] = {0};int count = recv(connfd, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd [%d] disconnect!\n", connfd);close(connfd);// 关闭客户端套接字文件描述符epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev);// 从epoll实例中移除continue;}printf("sever recv message [%s] from clientfd [%d]\n", buffer, connfd);count = send(connfd, buffer, count, MSG_SIGNAL);if (count == ERROR_CODE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), connfd);close(connfd);// 关闭客户端套接字文件描述符/*** 从 epoll 实例中删除文件描述符* 此操作仅需知道要删除的文件描述符,不需要额外的事件信息* 所以 event 参数会被忽略,可以传入 NULL*/epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);// 从epoll实例中移除continue;}printf("sever send message [%s] to clientfd [%d]\n", buffer, connfd);}}}
优点: 解决了select/poll中最消耗性能的两个痛点:规避了对fd集合在用户与内核之间整体的拷贝、对fd集合的管理(增删查)的时间复杂度优化至O(logN)级别。
缺点:针对购物节/秒杀活动,数百、千万人同时抢购,epoll显然无能为力。
五、完整代码实现
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/queue.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>// 错误码
#define ERROR_CODE -1
// fd失效
#define FD_INVAILD_CODE -1
// 等待连接数量
#define WAIT_CONNECT_COUNT 10
// pollfd结构体数组规模
#define POLLFD_CONNECT_COUNT 16384
// 初始化epoll连接数
#define EPOLLFD_CONNECT_INIT 1
// 初始化epoll_event 数组
#define EPOLL_EVENT_INIT 2048
// 超时等待时间 -1 无限等待
#define EPOLL_EVENT_TIMEOUT -1
// 接收消息的buffer长度
#define BUFFER_SIZE 1024
// 返回值
#define RETURN_CODE 0
// 默认协议类型
#define DEFAULT_PROTOCOL 0
// 初始化默认值
#define ZERO_INIT 0
// 默认端口号
#define DEFAULT_PORT 2000
// 循环常量
#define LOOP 1
// 消息发送模式
#define MSG_SIGNAL 0
// 消息数据长度
#define NO_MESSAGE_SIZE 0int main()
{/*** 1、创建新套接字的系统调用,返回一个文件描述符(整数)用于后续操作* AF_INET:地址族(Address Family),表示使用IPv4协议* SOCK_STREAM:套接字类型,表示面向连接的可靠字节流(自动选择TCP协议)* 0:表示使用默认协议*/int sockfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL);//成功时返回非负整数的文件描述符if (sockfd == ERROR_CODE) {printf("socket failed : %s\n ",strerror(errno));return sockfd;}printf("socket init success! sockfd = %d\n", sockfd);/*** 2、创建套接字地址结构* 用于指定要绑定的IP地址和端口号*/struct sockaddr_in servaddr;memset(&servaddr, ZERO_INIT, sizeof(servaddr)); // 初始化结构体servaddr.sin_family = AF_INET;// 设置地址族为 IPv4servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// 设置监听地址为任意本地网卡0.0.0.0servaddr.sin_port = htons(DEFAULT_PORT);//设置监听端口为 2000(0到1023为系统占用端口)/*** 3、绑定套接字和地址* sockfd:要绑定的套接字文件描述符* (struct sockaddr*)&servaddr:指向sockaddr_in结构体的指针,包含要绑定的地址信息* sizeof(struct sockaddr):地址结构体的大小* 返回值:成功时返回0,失败时返回-1*/if (bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr)) == ERROR_CODE) {printf("bind failed : %s\n ",strerror(errno));return ERROR_CODE;}/*** 4、启动监听* sockfd:要监听的套接字文件描述符* WAIT_CONNECT_COUNT:最大允许的等待连接请求数*/if (listen(sockfd, WAIT_CONNECT_COUNT) == ERROR_CODE) {printf("listen failed : %s\n ",strerror(errno));return ERROR_CODE;}printf("listen start! sockfd = %d\n", sockfd);// 定义客户端地址结构体,用于存储客户端的地址信息struct sockaddr_in clientaddr;socklen_t socklen = sizeof(clientaddr);printf("waiting accept! sockfd = %d\n", sockfd);#if 0/*** 5、select方式实现多路io复用* 定义文件描述符集合,readfds 用于存储所有需要监听的文件描述符* readset 作为 readfds 的副本,因为 select 系统调用会覆盖传入的readfds*/fd_set readfds, readset;FD_ZERO(&readfds);// 清空文件描述符集合FD_SET(sockfd, &readfds);// 将监听套接字 sockfd 添加到 readfds 集合中,用于监听新的连接请求int maxfd = sockfd;// 初始化最大文件描述符为监听套接字 sockfdwhile (LOOP)// 循环,持续监听文件描述符集合中的事件{readset = readfds;// 将 readfds 集合复制到 readset 集合,因为 select 会修改传入的集合/*** select 系统调用用于监听文件描述符集合中的事件* maxfd+1:表示文件描述符集合中的最大文件描述符加1,用于指定监听的范围* &readset:指向 fd_set 结构体的指针,用于指定要监听的文件描述符集合* NULL:不监听可写事件* NULL:不监听异常事件* NULL:指向 timeval 结构体的指针,用于指定超时时间, 为 NULL 时表示无限等待* 返回值:成功时返回就绪文件描述符的数量,失败时返回-1*/int nready = select(maxfd+1, &readset, NULL, NULL, NULL);// 检查是否有错误发生if (nready == ERROR_CODE) {printf("select failed : %s\n ",strerror(errno));continue;}// 检查监听套接字是否有新的客户端连接请求if (FD_ISSET(sockfd, &readset)) {// accept// 接受新的客户端连接,返回新的客户端套接字文件描述符int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : %d\n", clientfd);// 将新的客户端套接字文件描述符添加到 readfds 集合中,用于监听其数据可读事件FD_SET(clientfd, &readfds);// 更新最大的文件描述符maxfd = (clientfd > maxfd) ? (clientfd) : (maxfd);}// 遍历从监听套接字之后的所有文件描述符,直到最大文件描述符,检查是否有数据可读for (int i = sockfd + 1; i <= maxfd; ++i) {// recv// 检查当前文件描述符i是否在就绪集合中(有数据可读)if (FD_ISSET(i, &readset)) {char buffer[BUFFER_SIZE] = {0};/*** 6、recv系统调用,接收客户端数据* clientfd:要接收数据的客户端套接字文件描述符* buf:指向接收数据的缓冲区的指针* BUFFER_SIZE:要接收的最大字节数* MSG_SIGNAL:标志位,指定接收行为* 返回值:成功时返回实际接收的字节数,若返回0表示连接已关闭,若返回-1表示出错*/int count = recv(i, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd %d disconnect!\n", i);close(i); // 关闭客户端套接字文件描述符FD_CLR(i, &readfds);// 从监控集合移除客户端fdcontinue;} else {printf("clientfd %d recv : %s\n", i, buffer);}/*** 7、发送数据给客户端* clientfd:要发送数据的客户端套接字文件描述符* buf:指向要发送数据的缓冲区的指针* recvCount:要发送的字节数* MSG_SIGNAL:标志位,指定发送行为* 返回值:成功时返回实际发送的字节数,若返回-1表示出错*/count = send(i, buffer, count, MSG_SIGNAL);if (count == ERROR_CODE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), i);close(i);// 关闭客户端套接字文件描述符FD_CLR(i, &readfds);// 从监控集合移除客户端fdcontinue;} else {printf("send message: %s, count = %d, clientfd = %d\n", buffer, count, i);}}} }#elif 0/*** 5、poll方式实现多路io复用*/struct pollfd fds[POLLFD_CONNECT_COUNT] = {0};fds[sockfd].fd = sockfd;// 监听套接字fds[sockfd].events = POLLIN;// 监听可读事件int maxfd = sockfd;while (LOOP) {/*** poll 系统调用用于监听文件描述符集合中的事件* maxfd+1:表示文件描述符集合中的最大文件描述符加1,用于指定监听的范围* fds:指向 pollfd 结构体数组的指针,用于指定要监听的文件描述符集合* maxfd+1:数组中元素的数量,即要监听的文件描述符数量* -1:超时时间,单位为毫秒,-1 表示无限等待* 返回值:成功时返回就绪文件描述符的数量,失败时返回-1*/int nready = poll(fds, maxfd + 1, -1);if (nready == ERROR_CODE) {// poll 失败printf("poll failed : %s\n ",strerror(errno));continue;}if (fds[sockfd].revents & POLLIN) {// 监听套接字可读事件就绪 有新的客户端连接int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : %d\n", clientfd);fds[clientfd].fd = clientfd;fds[clientfd].events = POLLIN;maxfd = (clientfd > maxfd) ? (clientfd) : (maxfd);}for (int i = sockfd + 1; i <= maxfd; ++i) {if (fds[i].revents & POLLIN) {// 客户端可读事件就绪char buffer[BUFFER_SIZE] = {0};int count = recv(i, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd %d disconnect!\n", i);close(i);// 关闭客户端套接字文件描述符fds[i].fd = FD_INVAILD_CODE;// fd置为失效fds[i].events = ZERO_INIT;// 事件类型初始化continue;}printf("clientfd %d recv : %s\n", i, buffer);count = send(i, buffer, count, MSG_SIGNAL);if (count == ERROR_CODE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), i);close(i);// 关闭客户端套接字文件描述符fds[i].fd = FD_INVAILD_CODE;// fd置为失效fds[i].events = ZERO_INIT;// 事件类型初始化continue;}printf("clientfd %d send : %s\n", i, buffer);}}} #elif 1/*** 5、epoll方式实现多路io复用* epoll_create 系统调用 创建一个 epoll 实例* EPOLLFD_CONNECT_INIT:初始化的连接数 1* 返回值:成功时返回一个非负整数,即 epoll 实例的文件描述符*/int epfd = epoll_create(EPOLLFD_CONNECT_INIT);struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = sockfd;/*** epoll_ctl 系统调用用于添加、修改或删除文件描述符到 epoll 实例中* epfd:要操作的 epoll 实例的文件描述符* EPOLL_CTL_ADD:操作类型,指定添加文件描述符到 epoll 实例中* sockfd:要添加的文件描述符* &ev:指向 epoll_event 结构体的指针,关联fd与事件类型* 返回值:成功时返回0,失败时返回-1*/epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);while (LOOP){struct epoll_event events[EPOLL_EVENT_INIT] = {0};/*** epoll_wait 系统调用用于等待事件发生* epfd:要等待的 epoll 实例的文件描述符* events:指向 epoll_event 结构体数组的指针,用于接收就绪事件,内核返回复写后的就绪fd* EPOLL_EVENT_INIT:数组中元素的数量,即要接收的就绪事件数量* EPOLL_EVENT_TIMEOUT:超时时间,单位为毫秒,-1 表示无限等待* 返回值:成功时返回就绪事件的数量,失败时返回-1*/int nready = epoll_wait(epfd, events, EPOLL_EVENT_INIT, EPOLL_EVENT_TIMEOUT);printf("epoll wait...\n");for (int i = 0; i < nready; ++i) {int connfd = events[i].data.fd;if (connfd == sockfd) {int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &socklen);printf("accept success, clientfd : [%d]\n", clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else if (events[i].events & EPOLLIN) {char buffer[BUFFER_SIZE] = {0};int count = recv(connfd, buffer, BUFFER_SIZE, MSG_SIGNAL);if (count == NO_MESSAGE_SIZE) {printf("clientfd [%d] disconnect!\n", connfd);close(connfd);// 关闭客户端套接字文件描述符epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev);// 从epoll实例中移除continue;}printf("sever recv message [%s] from clientfd [%d]\n", buffer, connfd);count = send(connfd, buffer, count, MSG_SIGNAL);if (count == ERROR_CODE) {printf("send stop : %s\n , clientfd = %d", strerror(errno), connfd);close(connfd);// 关闭客户端套接字文件描述符/*** 从 epoll 实例中删除文件描述符* 此操作仅需知道要删除的文件描述符,不需要额外的事件信息* 所以 event 参数会被忽略,可以传入 NULL*/epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);// 从epoll实例中移除continue;}printf("sever send message [%s] to clientfd [%d]\n", buffer, connfd);}}}#endifgetchar();// 暂停程序,等待用户输入后退出printf("exit! sockfd = %d\n", sockfd);return RETURN_CODE;
}