I/O 多路复用,网络编程中的select、poll、epoll的发展历史、原理详解以及代码实现(五)

本节是该系列文章的完结篇,将带来select、poll、epoll三者中性能最好的epoll的代码实现。上节我们基于poll实现了一个简单的TCP回显服务器的应用,本节将使用epoll代替poll,仍保持TCP回显服务器应用的功能不变。

1. 创建tcp回显服务器

本步骤包括创建服务器套接字,绑定IP地址和端口,启动监听三部分,具体的代码实现请见第三节I/O 多路复用,网络编程中的select、poll、epoll的发展历史、原理详解以及代码实现(三)  

2. 使用epoll文件描述符进行监控

(1)创建epoll实例 

int epfd = epoll_create(1);
if (epfd == -1) {
    perror("epoll_create failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

epoll_create(1)epoll_create() 用来创建一个 epoll 实例,返回一个文件描述符 epfd,该文件描述符用于后续的 epoll_ctl()epoll_wait() 操作。

  • 参数 1 是建议给 epoll 实例分配的初始内存大小,但对于大多数应用来说,这个参数并不会影响实际的功能。
  • 如果 epoll_create() 返回 -1,则表示创建失败,程序通过 perror() 输出错误信息,并退出程序。

(2)注册监听事件 

struct epoll_event ev;
ev.events = EPOLLIN;  // 监听可读事件
ev.data.fd = sockfd;  // 设置监听的套接字文件描述符

if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
    perror("epoll_ctl failed");
    close(sockfd);
    close(epfd);
    exit(EXIT_FAILURE);
}
  • struct epoll_event evepoll_eventepoll 事件的结构体,它包含了事件类型(events)和一些相关的数据(data)。

    • ev.events = EPOLLIN:设置要监听的事件类型为 EPOLLIN,表示该文件描述符(即 sockfd)上有数据可读。EPOLLINepoll 中最常见的事件之一,表示套接字可以读取数据。
    • ev.data.fd = sockfd:这里指定 sockfd 作为要监听的文件描述符。ev.data 是一个联合体,包含了多种类型的数据。对于我们这里的情况,它存储的是一个文件描述符,用于标识哪个文件描述符(例如套接字)发生了事件。
  • epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev)

    • epoll_ctl()epoll 的控制函数,用来修改 epoll 实例中的事件。
    • EPOLL_CTL_ADD 表示将一个新的文件描述符和事件添加到 epoll 实例中。
    • 传入的参数 sockfd 是要监听的套接字文件描述符,&ev 是需要监听的事件。

    如果 epoll_ctl() 调用失败,则输出错误信息并退出。

(3)等待并处理事件 

struct epoll_event events[MAX_EVENTS];

while (1) {
    int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
    if (nready == -1) {
        perror("epoll_wait failed");
        close(sockfd);
        close(epfd);
        exit(EXIT_FAILURE);
    }
  • struct epoll_event events[MAX_EVENTS]epoll_wait() 函数将返回发生的事件,这里我们用 events 数组来存储这些事件。MAX_EVENTS 是数组的大小,表示我们最多能同时处理 MAX_EVENTS 个事件。

  • epoll_wait(epfd, events, MAX_EVENTS, -1)

    • epoll_wait() 用来等待一个或多个事件的发生。
    • 第一个参数 epfd 是我们之前创建的 epoll 实例的文件描述符。
    • 第二个参数 events 是一个数组,用来存储发生的事件。
    • MAX_EVENTS 是我们希望 epoll_wait() 最多处理的事件数量。
    • -1 表示阻塞,直到有事件发生才返回。

    如果 epoll_wait() 调用失败,返回 -1,则打印错误信息并退出程序。

(4)处理每个事件 

    for (int i = 0; i < nready; i++) {
        int connfd = events[i].data.fd;
  • for (int i = 0; i < nready; i++):遍历所有发生的事件,nready 表示发生的事件数。
  • int connfd = events[i].data.fd;:从 events[i] 中获取文件描述符 fd,即发生事件的套接字。对于监听套接字 sockfd 来说,这个文件描述符表示有新的连接请求到来;对于其他客户端连接套接字来说,表示该套接字上有数据可以读或写。

(5)处理新连接请求

        if (connfd == sockfd) {  // 新连接
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
            if (clientfd == -1) {
                perror("accept failed");
                continue;
            }
            printf("Client connected: %d\n", clientfd);
  • if (connfd == sockfd):如果当前事件是由监听套接字(sockfd)触发的,表示有新的客户端连接请求。accept() 用来接受这个新连接。
  • accept(sockfd, (struct sockaddr*)&clientaddr, &len):接受客户端连接并返回一个新的套接字文件描述符 clientfd,它用于与客户端进行通信。
    • clientaddr 存储客户端的地址信息,len 是地址结构的大小。

(6)将客户端套接字添加到 epoll 中 

            ev.events = EPOLLIN;
            ev.data.fd = clientfd;
            if (epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev) == -1) {
                perror("epoll_ctl failed for client");
                close(clientfd);
                continue;
            }
  • ev.events = EPOLLIN:将客户端套接字 clientfd 设置为监听可读事件。
  • epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev):将客户端套接字注册到 epoll 实例中,监听它的 EPOLLIN 事件。这样,epoll 就会监视客户端套接字上是否有数据到来。
    • 如果注册失败,打印错误信息并关闭该客户端连接。

(7)处理客户端数据

        } else if (events[i].events & EPOLLIN) {  // 有数据可读
            char buffer[1024] = {0};
            int count = recv(connfd, buffer, sizeof(buffer), 0);
            if (count == 0) {  // 客户端断开连接
                printf("Client disconnected: %d\n", connfd);
                close(connfd);
                epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
            } else if (count < 0) {
                perror("recv failed");
            } else {
                printf("Received: %s\n", buffer);
                send(connfd, buffer, count, 0);  // 回传接收到的数据
            }
        }
    }
  • else if (events[i].events & EPOLLIN):如果事件类型包含 EPOLLIN,说明套接字上有可读数据。
  • recv(connfd, buffer, sizeof(buffer), 0):从客户端接收数据,保存在 buffer 中。recv() 会阻塞直到有数据可读或发生错误。
    • 如果 recv() 返回 0,表示客户端已经断开连接,服务器关闭该连接并从 epoll 实例中删除。
    • 如果 recv() 返回小于 0 的值,表示接收数据失败,打印错误信息。
    • 如果接收到数据,打印数据并将接收到的数据回传给客户端。

3. 总结 

整个 epoll 部分的代码实现了高效的多路复用机制,用于处理多个客户端的并发连接。主要流程是:

  1. 创建 epoll 实例;
  2. 注册监听的套接字 sockfd
  3. 使用 epoll_wait() 等待和处理事件;
  4. 如果是新连接,接受客户端并将其套接字添加到 epoll
  5. 如果是客户端套接字上有数据到来,读取数据并回传给客户端。

通过 epoll,可以高效地处理大量的并发连接,而不会像传统的 select 一样受到连接数的限制。

epoll在 Linux 系统中提供了一种高效的 I/O 事件通知机制,并且正是epoll的出现,使得Linux成为服务器操作系统的主流选择。虽然如此,其工作原理仍然基于同步 I/O,而不是异步 I/O。本系列结束后,后续会开启异步I/O的系列文章,包括协程,io_uring等,并会带来相关的项目。

 https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值