10_ select/poll/epoll实现服务端的io多路复用

一、io多路复用

在现有模型中,似乎每一个线程都做了同样的事情,1、监听客户端消息;2、业务消息处理。

“一消息一线程”的缺点究其根本,在于让每个线程都做了同样重复、且消耗资源巨大的事情——单独持有fd、监听客户端消息。

image

能不能不让每个线程独占客户端fd,又把监听的工作抛出去呢?

如果有一个观察者,能感知到就绪的客户端消息,消息就绪后再通过回调,来触发线程处理对应的业务消息,就能避免用户态持有大量线程在cpu里空转。

由此,我们可以引入一种新的设计模式————【状态-观察者】:由观察者统一去监听客户端的消息,当数据已经就绪(从内核缓冲区/空间拷贝到用户缓冲区/空间),我们直接在用户空间找到就绪的fd,只让任务线程来处理就绪的IO事件,而没有消息时,负责事件处理的线程就不去占用cpu资源了。

这个“观察者”,其实就是IO多路复用器:

image

通过IO多路复用模型,我们从理论上解决了“一消息一线程”下【事件监听-事件处理】的耦合,解放了内存/计算资源,不用再为每个客户端的连接对应创建独立的线程,而是让监听由更高效的内核态来完成,事件处理则只交给用户态的任务处理线程,这样不仅压榨了硬件资源,而且规避了频繁的线程切换,可以做到用单线程承载多客户端的IO事件,这就是分布式并发系统的基石。

二、select

2.1 理解select模型

image

  1. 创建fd_set:它本质是一个位图(位数组),用来存放每一个客户端与服务端建立的文件描述符;
  2. 系统调用select:监听fd_set是否存在就绪fd的系统调用,当没有就绪的fd时,用户空间阻塞,此时用户空间不占用计算资源,但在内核中会持续循环,直到出现就绪的fd或超时返回;
  3. 系统调用read:网卡直接将收到客户端消息的fd拷贝到内核空间,此时内核判断出现了就绪的fd,遍历并标记对应fd,完成标记后由read系统调用将fd_set整体从内核空间拷贝回用户空间;
  4. 用户空间接收到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中:

image

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);// 从监控集合移除客户端fd
                    continue;
                } 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);// 从监控集合移除客户端fd
                    continue;
                } 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是相同的。

image

使用层面的区别在于: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来展开,系统为我们提供了如下可配置和触发的事件,常用如下:

  1. 可读事件,POLLIN:0x001,有普通数据可读,表示服务端可以recv
  2. 可写事件,POLLOUT:0x008,表示文件描述符已经准备好接收数据,表示服务端可以send
  3. 错误/断开事件: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);

image

  1. 异步唤醒和跨缓冲区拷贝————规避2次O(n)查询和1次完整fd集合拷贝。
  2. 使用红黑树监控和管理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 0

int 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;// 设置地址族为 IPv4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// 设置监听地址为任意本地网卡0.0.0.0
    servaddr.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;// 初始化最大文件描述符为监听套接字 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);// 从监控集合移除客户端fd
                    continue;
                } 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);// 从监控集合移除客户端fd
                    continue;
                } 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);

            }
        }
    }
    

#endif

    getchar();// 暂停程序,等待用户输入后退出

    printf("exit! sockfd = %d\n", sockfd);

    return RETURN_CODE;
}
 
 
 
posted @ 2025-09-25 16:26  碧蓝i之海  阅读(42)  评论(0)    收藏  举报