【网络-编程】一线程多连接 TCP 通信

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

对 Linux 系统中 3 种 I/O 多路复用方式(selectpollepoll)进行介绍,包括函数说明、处理流程、示例代码和优缺点分析,并对 3 种方式进行对比分析。


服务器端采用 一线程一连接的通信方式,当客户端连接变多时,线程也会变多。由于线程对存储资源的占用,以及 CPU 在线程之间的调度开销,会造成系统性能下降。
采用 I/O 多路复用技术,可以实现一个线程维护多个客户端连接,从而减少开销,提高性能。

1 I/O 多路复用

I/O 多路复用,通过某种机制,实现一个线程可以同时监视多个 I/O 描述符,内核一旦发现某个描述符就绪(如读就绪、写就绪),就通知程序进行相应的(读、写)操作。

I/O 多路复用有多种实现方式,系统调用包括 selectpollepoll 等,其中 epoll 是 Linux 所特有的,select 是 POSIX(Portable Operating System Interface)所规定的,一般操作系统均可实现。

在 Linux 2.4 内核前,主要是 selectpoll(目前在小规模服务器上仍然有用武之地,并且在维护老系统代码时,也会经常用到这两个函数);
从 Linux 2.6 内核正式引入 epoll 以来,epoll 已经成为目前实现高性能网络服务器的必备技术。

2 基于 select 的服务器

2.1 select 函数

/**
 * @file  <sys/select.h>
 * @brief  允许程序监视多个文件描述符,等待其中一个或多个文件描述符对某类 I/O 操作变得“就绪”,
 *         即可以在没有阻塞的情况下执行对应的 I/O 操作(如读,或少量数据的写)。
 *
 * @param[in] maxfd 需要被设置为 “集合中所有文件描述符的最大值 + 1”,
 *                  在等待是否有套接字准备就绪时,只需要监测 maxfd 个套接字。
 *                  在 Linux 系统中,默认最大值为 1024。
 * @param[inout] readfds 指向 fd_set 结构的指针,里面包含多个文件描述符。
 *                       输入为,需要监视其“是否可读”的文件描述符的集合,
 *                              如果为 NULL,表示不关心任何文件的“可读事件”;
 *                       输出为,已经“可读”(读操作不会被阻塞)的文件描述符的集合。
 *                              即 readfds 中套接字的状态包括:
 *                              1)有数据可读,可以调用 recv 接收数据;
 *                              2)连接已经关闭,即调用 recv 返回 0,需调用 close;
 *                              3)正在请求建立连接,可以调用 accept 建立链接。
 * @param[inout] writefds 类似 readfds,不同的是,其监视文件描述符“是否可写”;
 *                        另外,如果写入数据量过大,写操作仍然会被阻塞。
 *                        输出 writefds 中套接字的状态包括:
 *                        1)可以写数据,即可以调用 send 发送数据;
 *                        2)可以请求连接,即可以调用 connect 建立连接。
 * @param[inout] exceptfds 类似 readfds,不同的是,其监视文件描述符“是否异常”。
 * @param[inout] timeout 表示 select 的等待时间,
 *                       设置为 NULL 时,表明 select 函数是阻塞的;
 *                       设置为 timeout->tv_sec = 0,timeout->tv_usec = 0 时,表明函数是非阻塞的;
 *                       设置为非 0 时间,表明函数有超时时间,超时后,就会返回。
 * @return  成功 >0,值为包含在三个描述符集合中的“就绪”文件描述符的数量;
 *          超时 =0,没有任何文件描述符准备就绪;
 *          出错 -1,可以通过 errno 获取错误信息。
 *                  文件描述符集合保持不变,而 timeout 变为未定义。
 */
int select(int maxfd, fd_set *restrict readfds, fd_set *restrict writefds, 
           fd_set *restrict exceptfds, struct timeval *restrict timeout);

void FD_ZERO(fd_set *set);         // 将 set 集合初始化为空集合
void FD_SET(int fd, fd_set *set);  // 将套接字 fd 加入到 set 集合
void FD_CLR(int fd, fd_set *set);  // 从 set 集合中删除 fd 套接字
int FD_ISSET(int fd, fd_set *set); // 检查 fd 是否在 set 集合中

通过 select(2)select(3p) 可以查看详细说明。

2.2 处理流程

采用 select 方式,服务器端 I/O 多路复用处理流程示意:

select 方式处理流程

2.3 示例代码

采用 select 方式,实现服务器端可读写事件监测。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/select.h> // 包含 select 函数相关
#include <sys/poll.h>   // 包含 poll 函数相关
#include <sys/epoll.h>  // 包含 epoll 函数相关

#define SOCK_IP INADDR_ANY
#define SOCK_PORT 2048
#define LISTEN_BACKLOG 10
#define BUF_SIZE 256

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main()
{
    int sockfd;
    socklen_t addrlen;
    struct sockaddr_in addr, server_addr;

    // 创建一个套接字,用于监听客户端的连接
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
        handle_error("socket");

    // 允许地址的立即重用
    // int setsockopt(int sockfd, int level, int option_name,
    //                const void *option_value, socklen_t option_len);
    int on = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
        handle_error("setsockopt");

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(SOCK_IP);
    addr.sin_port = htons(SOCK_PORT);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
        handle_error("bind");

    // 获取本地套接字地址
    addrlen = sizeof(server_addr);
    if (getsockname(sockfd, (struct sockaddr *)&server_addr, &addrlen) == -1)
        handle_error("getsockname");
    printf("Server Addr [%s:%d]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    if (listen(sockfd, LISTEN_BACKLOG) == -1)
        handle_error("listen");

#if 1 // I/O 多路复用处理

    // 使用 select 实现 I/O 多路复用
    struct // 参照 fd_set,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
    {
        char rbuf[BUF_SIZE]; // 读缓存
        char wbuf[BUF_SIZE]; // 写缓存
    } fd_buf[FD_SETSIZE];    // 读写缓存数组

    // “in_* 仅输入” 集合,用于 FD 事件保存和调整
    // “inout_* 既输入又输出” 集合,用于传入参数,获取返回值
    fd_set in_rfds, inout_rfds; // 读事件监测集合
    fd_set in_wfds, inout_wfds; // 写事件监测集合
    FD_ZERO(&in_rfds);
    FD_ZERO(&in_wfds);

    FD_SET(sockfd, &in_rfds); // 设置 “监听套接字” 监测 “读” 事件
    int maxfd = sockfd;       // 初始化最大 FD

    while (1)
    {
        inout_rfds = in_rfds; // 调用 select 前,更新参数
        inout_wfds = in_wfds;

        // 传入数组参数,获取数组返回值【性能影响】
        int nready = select(maxfd + 1, &inout_rfds, &inout_wfds, NULL, NULL);
        if (nready == -1)
            handle_error("select");

        if (FD_ISSET(sockfd, &inout_rfds)) // “监听套接字” 可读
        {
            int clientfd;
            struct sockaddr_in client_addr;

            // 从连接请求队列中取出排在最前面的客户端请求
            addrlen = sizeof(client_addr);
            clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
            if (clientfd == -1)
                handle_error("accept");
            printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

            FD_SET(clientfd, &in_wfds); // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
            fd_buf[clientfd].rbuf[0] = '\0';
            sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
                                           "Send \"bye\" to end the communication,\n"
                                           "Send \"q\" to quit the service,\n"
                                           "Send anything else to continue the communication.\n",
                    inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            if (clientfd > maxfd) maxfd = clientfd; // 更新最大 FD
        }

        // 在循环中遍历每一个客户端连接套接字事件【性能影响】
        for (int clientfd = sockfd + 1; clientfd <= maxfd; clientfd++)
        {
            if (FD_ISSET(clientfd, &inout_rfds)) // “客户端连接套接字” 可读
            {
                // 接收数据
                char *buf = fd_buf[clientfd].rbuf;
                int rlen = recv(clientfd, buf, BUF_SIZE, 0);
                if (rlen == -1)
                    handle_error("recv");

                if (rlen == 0) // 客户端 close 断开连接
                {
                    printf("close fd(%d)\n", clientfd);
                    FD_CLR(clientfd, &in_rfds);
                    if (close(clientfd) == -1)
                        handle_error("close clientfd");
                    continue;
                }

                buf[rlen] = '\0';
                printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);

                // 通信控制
                if (strcmp(buf, "bye") == 0)
                {
                    printf("close fd(%d)\n", clientfd);
                    FD_CLR(clientfd, &in_rfds);
                    if (close(clientfd) == -1)
                        handle_error("close clientfd");
                    continue;
                }
                else if (strcmp(buf, "q") == 0)
                {
                    // 关闭所有客户端 FD
                    for (int cfd = sockfd + 1; cfd <= maxfd; cfd++)
                    {
                        // 查找仍在监测读写事件(即未关闭)的客户端连接
                        if (FD_ISSET(cfd, &in_rfds) || FD_ISSET(cfd, &in_wfds))
                        {
                            printf("close fd(%d)\n", cfd);
                            if (close(cfd) == -1)
                                handle_error("close clientfd");
                        }
                    }

                    if (close(sockfd) == -1)
                        handle_error("close sockfd");
                    return 0;
                }

                FD_CLR(clientfd, &in_rfds);                           // 不再接收数据
                FD_SET(clientfd, &in_wfds);                           // 需要发送数据
                strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
            }

            if (FD_ISSET(clientfd, &inout_wfds)) // “客户端连接套接字” 可写
            {
                // 发送数据
                char *buf = fd_buf[clientfd].wbuf;
                int wlen = send(clientfd, buf, strlen(buf), 0);
                if (wlen != strlen(buf))
                    handle_error("send");
                printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);

                FD_CLR(clientfd, &in_wfds); // 不再发送数据
                FD_SET(clientfd, &in_rfds); // 需要接收数据
            }
        }
    }

#endif // I/O 多路复用处理

    return 0;
}

2.4 select 优缺点

优点:

  1. 跨平台:几乎在所有的平台上都可用。

缺点:

  1. 拷贝开销:需要在用户和内核地址空间之间,对文件描述符 fd 集合进行整体拷贝;
  2. 内核检测效率低:内核每次检测都需要遍历输入的 fd 集合;
  3. 应用查找效率低:应用程序也需要对输出的 fd 集合进行遍历,查找就绪状态的 fd;
  4. 参数过多:有 3 个集合参数,需要分别对每个集合进行遍历;
  5. 数量限制:监视 fd 的数量默认最多为 1024(Linux)。

3 基于 poll 的服务器

3.1 poll 函数

/**
 * @file  <sys/poll.h>
 * @brief  与 select 类似,poll 同样允许程序监视多个文件描述符,
 *         等待其中一个或多个文件描述符对某类 I/O 操作变得“就绪”。
 *
 * @param[inout] fds 指向 pollfd 结构体数组的指针
 *  struct pollfd {
 *      int fd;            // 文件描述符,如果为负,将忽略 events,revents 返回 0
 *      short int events;  // [输入] 等待的事件,如果为 0,revents 只会返回 POLLHUP, POLLERR, POLLNVAL
 *      short int revents; // [输出] 实际发生的事件
 *  };
 *  可以监视的事件有 POLLIN(可读)、POLLPRI(异常)、POLLOUT(可写) 等。
 * @param[in] nfds 指定第一个参数 fds 数组中元素的个数
 *                 不同于 select,文件描述符数量没有 1024 上限
 * @param[in] timeout 表示 poll 的等待时间(毫秒)
 *                    设置为 ‒1 时,表明 poll 函数是阻塞的;
 *                    设置为 0 时,表明函数是非阻塞的,立即返回;
 *                    设置为 >0 时,表明函数有超时时间,超时后,就会返回。
 * @return  成功 >0,值为 fds 中 revents 非 0 的文件描述符的数量
 *          超时 =0,没有任何文件描述符监视事件发生;
 *          出错 -1,可以通过 errno 获取错误信息。
 */
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

通过 poll(2)poll(3p) 可以查看详细说明。

3.2 处理流程

采用 poll 函数,服务器端 I/O 多路复用处理流程示意:

poll 方式处理流程

3.3 示例代码

采用 poll 方式,实现服务器端可读写事件监测。

// 使用 poll 实现 I/O 多路复用
#define MAX_FD_SIZE 1024

struct // 简化实现,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
{
    char rbuf[BUF_SIZE]; // 读缓存
    char wbuf[BUF_SIZE]; // 写缓存
} fd_buf[MAX_FD_SIZE];   // 读写缓存数组

struct pollfd fds[MAX_FD_SIZE] = {0};
for (int i = 0; i < MAX_FD_SIZE; i++)
    fds[i].fd = -1;

fds[sockfd].fd = sockfd;     // 简化实现,将 fd 保存在索引为 fd 的位置
fds[sockfd].events = POLLIN; // 设置 “监听套接字” 监测 “读” 事件

int maxfd = sockfd; // 初始化最大 FD

while (1)
{
    // 传入 fds 数组参数,获取 fds 数组返回值【性能影响】
    int nready = poll(fds, maxfd + 1, -1);
    if (nready == -1)
        handle_error("poll");

    if (fds[sockfd].revents & POLLIN) // “监听套接字” 可读
    {
        int clientfd;
        struct sockaddr_in client_addr;

        // 从连接请求队列中取出排在最前面的客户端请求
        addrlen = sizeof(client_addr);
        clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
        if (clientfd == -1)
            handle_error("accept");
        printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        fds[clientfd].fd = clientfd;
        fds[clientfd].events = POLLOUT; // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
        fd_buf[clientfd].rbuf[0] = '\0';
        sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
                                       "Send \"bye\" to end the communication,\n"
                                       "Send \"q\" to quit the service,\n"
                                       "Send anything else to continue the communication.\n",
                inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
        if (clientfd > maxfd) maxfd = clientfd; // 更新最大 FD
    }

    // 在循环中遍历每一个客户端连接套接字事件【性能影响】
    for (int clientfd = sockfd + 1; clientfd <= maxfd; clientfd++)
    {
        if (fds[clientfd].revents & POLLIN) // “客户端连接套接字” 可读
        {
            // 接收数据
            char *buf = fd_buf[clientfd].rbuf;
            int rlen = recv(clientfd, buf, BUF_SIZE, 0);
            if (rlen == -1)
                handle_error("recv");

            if (rlen == 0) // 客户端 close 断开连接
            {
                printf("close fd(%d)\n", clientfd);
                fds[clientfd].fd = -1;
                if (close(clientfd) == -1)
                    handle_error("close clientfd");
                continue;
            }

            buf[rlen] = '\0';
            printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);

            // 通信控制
            if (strcmp(buf, "bye") == 0)
            {
                printf("close fd(%d)\n", clientfd);
                fds[clientfd].fd = -1;
                if (close(clientfd) == -1)
                    handle_error("close clientfd");
                continue;
            }
            else if (strcmp(buf, "q") == 0)
            {
                // 关闭所有客户端 FD
                for (int cfd = sockfd + 1; cfd <= maxfd; cfd++)
                {
                    // 查找未关闭的客户端连接
                    if (fds[cfd].fd != -1)
                    {
                        printf("close fd(%d)\n", cfd);
                        if (close(cfd) == -1)
                            handle_error("close clientfd");
                    }
                }

                if (close(sockfd) == -1)
                    handle_error("close sockfd");
                return 0;
            }

            fds[clientfd].events = POLLOUT;                       // 需要发送数据
            strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
        }

        if (fds[clientfd].revents & POLLOUT) // “客户端连接套接字” 可写
        {
            // 发送数据
            char *buf = fd_buf[clientfd].wbuf;
            int wlen = send(clientfd, buf, strlen(buf), 0);
            if (wlen != strlen(buf))
                handle_error("send");
            printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);

            fds[clientfd].events = POLLIN; // 需要接收数据
        }
    }
}

使用上述代码片段替换 select 示例代码#if 1 // I/O 多路复用处理 宏定义内部分,可以形成完整示例代码。

3.4 poll 优缺点

pollselect 函数的本质是一样的。

优点:

  1. 文件描述符 fd 数量仅受其类型影响,没有额外限制。

缺点:

  1. 缺少针对大量 fd 的优化机制,fd 数量过大后性能会下降;
  2. 拷贝开销:需要在用户和内核地址空间之间,对 fd 集合进行整体拷贝;
  3. 内核检测效率低:内核每次检测都需要遍历输入的 fd 集合;
  4. 应用查找效率低:应用程序也需要对输出的 fd 集合进行遍历,查找就绪状态的 fd。

4 基于 epoll 的服务器

4.1 epoll 函数

/**
 * @file  <sys/epoll.h>
 * @brief  创建一个 epoll 实例,返回一个 epoll 文件描述符。
 *         这个文件描述符用于后续所有对 epoll 接口的调用;
 *         当不再需要时,应该使用 close 释放资源。
 *
 * @param[in] size 大于 0 的整数,如 1
 *                 需要监测的文件描述符的数目(从 Linux 2.6.8 起被忽略)
 * @return  如果成功,返回一个 int 类型非负的文件描述符;
 *          如果失败,返回 ‒1,可以通过 errno 获取错误信息。
 */
int epoll_create(int size);
/**
 * @file  <sys/epoll.h>
 * @brief  在 “epfd 指向的 epoll 实例” 的监控列表中,添加、修改或删除条目,
 *         即,请求为目标文件描述符 fd 执行操作 op。
 *
 * @param[in] epfd 通过 epoll_create 返回的文件描述符
 * @param[in] op 表示控制操作,可以设置为:
 *               EPOLL_CTL_ADD:添加新的 fd 到 epfd 的监控列表中,并监测 event 事件
 *               EPOLL_CTL_MOD:使用 event 修改已经添加的 fd 的监测事件
 *               EPOLL_CTL_DEL:从 epfd 的监控列表中删除 fd,event 可以为 NULL
 * @param[in] fd 控制操作 op 的实施对象,即需要操作的文件描述符
 * @param[in] event 指定文件描述符 fd 需要监测的事件
 *                  struct epoll_event {
 *                     uint32_t      events;  // Epoll events
 *                     epoll_data_t  data;    // User data variable
 *                  };
 *                  union epoll_data {
 *                     void     *ptr;
 *                     int       fd;
 *                     uint32_t  u32;
 *                     uint64_t  u64;
 *                  };
 *                  typedef union epoll_data  epoll_data_t;
 *
 *                  data 成员,是用户自定义数据,内核通过 epoll_wait() 会直接返回;
 *                  events 成员,是通过“按位或”组合在一起的零个或多个事件类型
 *                  事件类型有:
 *                  EPOLLIN:表示对应的文件描述符可以读
 *                  EPOLLOUT:表示对应的文件描述符可以写
 *                  EPOLLPRI:表示对应的文件描述符有紧急的数据可读
 *                  ...
 * @return   成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);
/**
 * @file  <sys/epoll.h>
 * @brief  获取在 “epfd 指向的 epoll 实例” 的监控列表中已经发生的事件
 *
 * @param[in] epfd 通过 epoll_create 返回的文件描述符
 * @param[out] events 将所有就绪的事件拷贝到 events 指向的数组中;
 *                    数组元素 epoll_event 结构的
 *                    data 成员,是最近一次通过 EPOLL_CTL_ADD, EPOLL_CTL_MOD 设置的用户数据;
 *                    events 成员,是已经发生的事件。
 * @param[in] maxevents 指定 events 数组中最多可容纳事件的数量
 * @param[in] timeout 指定 epoll_wait 的等待时间(毫秒)
 *                    设置为 ‒1 时,表明 epoll_wait 函数是阻塞的;
 *                    设置为 0 时,表明函数是非阻塞的,立即返回;
 *                    设置为 >0 时,表明函数有超时时间,超时后,就会返回。
 * @return  成功 >0,表示 events 中就绪文件描述符的个数
 *          超时 =0,没有任何文件描述符事件发生;
 *          出错 -1,可以通过 errno 获取错误信息。
 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

通过 epoll(7) 可以查看详细说明。

4.2 处理流程

采用 epoll 方式,服务器端 I/O 多路复用处理流程示意:

epoll 方式处理流程

4.3 示例代码

采用 epoll 方式,实现服务器端可读写事件监测。

// 使用 epoll 实现 I/O 多路复用
#define MAX_FD_SIZE 1024

struct // 简化实现,统一分配空间,将 fd(如 3)保存在索引为 fd(如 fd_buf[3])的位置
{
    int fd;                  // 客户端连接套接字描述符
    char rbuf[BUF_SIZE];     // 读缓存
    char wbuf[BUF_SIZE];     // 写缓存
} fd_buf[MAX_FD_SIZE] = {0}; // 读写缓存数组

struct epoll_event events[MAX_FD_SIZE] = {0};

int epfd = epoll_create(1);
if (epfd == -1)
    handle_error("epoll_create");

struct epoll_event ev;
ev.data.fd = sockfd;
ev.events = EPOLLIN; // 设置 “监听套接字” 监测 “读” 事件
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1)
    handle_error("EPOLL_CTL_ADD");

while (1)
{
    int nready = epoll_wait(epfd, events, MAX_FD_SIZE, -1);
    if (nready == -1)
        handle_error("epoll_wait");

    for (int i = 0; i < nready; i++)
    {
        if (events[i].data.fd == sockfd) // “监听套接字” 可读
        {
            int clientfd;
            struct sockaddr_in client_addr;

            // 从连接请求队列中取出排在最前面的客户端请求
            addrlen = sizeof(client_addr);
            clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
            if (clientfd == -1)
                handle_error("accept");
            printf("accept fd(%d) [%s:%d]\n", clientfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

            ev.data.fd = clientfd;
            ev.events = EPOLLOUT; // 设置 “客户套接字” 监测 “写” 事件,发送服务说明
            if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev) == -1)
                handle_error("EPOLL_CTL_ADD");

            fd_buf[clientfd].fd = clientfd;
            fd_buf[clientfd].rbuf[0] = '\0';
            sprintf(fd_buf[clientfd].wbuf, "Welcome [%s:%d] to the server!\n"
                                           "Send \"bye\" to end the communication,\n"
                                           "Send \"q\" to quit the service,\n"
                                           "Send anything else to continue the communication.\n",
                    inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            continue;
        }

        // 处理每一个客户端连接套接字事件
        int clientfd = events[i].data.fd;
        if (events[i].events & EPOLLIN) // “客户端连接套接字” 可读
        {
            // 接收数据
            char *buf = fd_buf[clientfd].rbuf;
            int rlen = recv(clientfd, buf, BUF_SIZE, 0);
            if (rlen == -1)
                handle_error("recv");

            if (rlen == 0) // 客户端 close 断开连接
            {
                printf("close fd(%d)\n", clientfd);
                fd_buf[clientfd].fd = -1;
                if (epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL) == -1)
                    handle_error("EPOLL_CTL_DEL");
                if (close(clientfd) == -1)
                    handle_error("close clientfd");
                continue;
            }

            buf[rlen] = '\0';
            printf("fd(%d) recv: %s(%d)\n", clientfd, buf, rlen);

            // 通信控制
            if (strcmp(buf, "bye") == 0)
            {
                printf("close fd(%d)\n", clientfd);
                fd_buf[clientfd].fd = -1;
                if (epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL) == -1)
                    handle_error("EPOLL_CTL_DEL");
                if (close(clientfd) == -1)
                    handle_error("close clientfd");
                continue;
            }
            else if (strcmp(buf, "q") == 0)
            {
                // 关闭所有客户端 FD
                for (int cfd = epfd + 1; cfd < MAX_FD_SIZE; cfd++)
                {
                    // 查找未关闭的客户端连接
                    if (fd_buf[cfd].fd > 0)
                    {
                        printf("close fd(%d)\n", cfd);
                        if (close(cfd) == -1)
                            handle_error("close clientfd");
                    }
                }

                if (close(epfd) == -1)
                    handle_error("close epfd");

                if (close(sockfd) == -1)
                    handle_error("close sockfd");

                return 0;
            }

            ev.data.fd = clientfd;
            ev.events = EPOLLOUT; // 需要发送数据
            if (epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev) == -1)
                handle_error("EPOLL_CTL_MOD");

            strcpy(fd_buf[clientfd].wbuf, fd_buf[clientfd].rbuf); // 发送“接收到的内容”
        }

        if (events[i].events & EPOLLOUT) // “客户端连接套接字” 可写
        {
            // 发送数据
            char *buf = fd_buf[clientfd].wbuf;
            int wlen = send(clientfd, buf, strlen(buf), 0);
            if (wlen != strlen(buf))
                handle_error("send");
            printf("fd(%d) send: %s(%d)\n", clientfd, buf, wlen);

            ev.data.fd = clientfd;
            ev.events = EPOLLIN; // 需要接收数据
            if (epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev) == -1)
                handle_error("EPOLL_CTL_MOD");
        }
    }
}

使用上述代码片段替换 select 示例代码#if 1 // I/O 多路复用处理 宏定义内部分,可以形成完整示例代码。

4.4 epoll 优缺点

epollselect/poll 的增强版本,在“并发连接数量多,活跃连接较少”的情况下,能够显著提高系统的性能。

优点:

  1. 没有大量无效输入/输出拷贝开销:
    • 拆分为 epoll_ctl() 和 epoll_wait() 两个独立接口(调用频率不同);
    • epoll_ctl() 设置输入参数,每次只设置目标文件描述符 fd 事件(调用频率相对低);
    • epoll_wait() 返回输出结果,每次只返回当前就绪 fd 集合(调用频率相对高);
  2. 内核检测效率高:采用回调方式检测就绪事件;
  3. 应用查找效率高:系统调用仅返回就绪事件,不需要再判断 fd 是否就绪;
  4. 增加“边缘触发 ET”工作方式,可以进一步提升性能。

缺点:

  1. 在活跃连接比较多时,回调函数被触发得过于频繁,效率会受到影响。

5 三种方式比较

系统调用selectpollepoll
输入/输出事件集合数量不同事件类型对应不同集合参数一个集合参数包含不同事件类型同 poll
输入/输出共享内存情况输出直接覆盖输入数据(再次调用需重新赋值)输入输出使用不同字段输出为独立内存空间
输入 fd 集合有效性可能包含无效 fd,并且会反复传入目标 fd 事件(整体拷贝)同 select通过 epoll_ctl 一次性处理目标 fd 事件
输出 fd 集合有效性可能包含无效 fd(整体拷贝)同 select通过 epoll_wait 仅返回就绪 fd
最大支持 fd 数量Linux 默认 1024与 fd 类型相关同 poll
内核检测方式采用轮询方式检测就绪事件同 select采用回调方式检测就绪事件
内核检测时间复杂度O(n)同 selectO(1)
应用查找就绪 fd 方式采用轮询方式查找就绪 fd 事件同 select直接定位就绪 fd 事件
应用查找就绪 fd 时间复杂度O(n)同 selectO(1)
工作模式LT同 selectLT、ET
适用场景活跃连接多 & 并发连接少同 select活跃连接少

参考

  1. 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.

宁静以致远,感谢 King 老师。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值