30 天自制 C++ 服务器--Day3

发布于:2025-07-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

高并发还得用epoll

在上一天,我们写了一个简单的echo服务器,但只能同时处理一个客户端的连接。但在这个连接的生命周期中,绝大部分时间都是空闲的,活跃时间(发送数据和接收数据的时间)占比极少,这样独占一个服务器是严重的资源浪费。事实上所有的服务器都是高并发的,可以同时为成千上万个客户端提供服务,这一技术又被称为IO复用。

IO复用和多线程有相似之处,但绝不是一个概念。IO复用是针对IO接口,而多线程是针对CPU。

IO复用的基本思想是事件驱动,服务器同时保持多个客户端IO连接,当这个IO上有可读或可写事件发生时,表示这个IO对应的客户端在请求服务器的某项服务,此时服务器响应该服务。在Linux系统中,IO复用使用select, poll和epoll来实现。epoll改进了前两者,更加高效、性能更好,是目前几乎所有高并发服务器的基石。请读者务必先掌握epoll的原理再进行编码开发。

epoll原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结:

epoll 的核心原理在于改变事件通知模型:它通过在内核使用红黑树高效管理注册的文件描述符,利用回调函数在事件发生时直接将就绪项放入链表,并通过 epoll_wait 仅返回实际就绪的事件列表,辅以 mmap 减少数据拷贝。这种设计彻底解决了 select/poll 在处理大量文件描述符时存在的性能瓶颈(每次调用传递完整集合、线性扫描开销大),使其成为构建现代高性能 Linux 网络服务器(如 Nginx, Redis, Memcached)不可或缺的基础设施。其高效的根源在于将时间复杂度从 O(N) 降低到了 O(1)(或 O(就绪事件数))。

epoll使用

epoll主要由三个系统调用组成:

//int epfd = epoll_create(1024);  //参数表示监听事件的大小,如超过内核会自动调整,已经被舍弃,无实际意义,传入一个大于0的数即可
int epfd = epoll_create1(0);       //参数是一个flag,一般设为0,详细参考man epoll

创建一个epoll文件描述符并返回,失败则返回-1。

epoll监听事件的描述符会放在一颗红黑树上,我们将要监听的IO口放入epoll红黑树中,就可以监听该IO上的事件。

epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);    //添加事件到epoll
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);    //修改epoll红黑树上的事件
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);   //删除事件

其中sockfd表示我们要添加的IO文件描述符,ev是一个epoll_event结构体,其中的events表示事件,如EPOLLIN等,data是一个用户数据union:

typedef union epoll_data {
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;
struct epoll_event {
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

epoll默认采用LT触发模式,即水平触发,只要fd上有事件,就会一直通知内核。这样可以保证所有事件都得到处理、不容易丢失,但可能发生的大量重复通知也会影响epoll的性能。如使用ET模式,即边缘触法,fd从无事件到有事件的变化会通知内核一次,之后就不会再次通知内核。这种方式十分高效,可以大大提高支持的并发度,但程序逻辑必须一次性很好地处理该fd上的事件,编程比LT更繁琐。注意ET模式必须搭配非阻塞式socket使用。

非阻塞式socket和阻塞式

在这里插入图片描述
在这里插入图片描述
示例代码:

// 阻塞式服务端伪代码
int client = accept(server_sock); // 阻塞直到新连接
recv(client, buf); // 阻塞直到收到数据
send(client, response); // 阻塞直到发送完成

在这里插入图片描述
在这里插入图片描述

设置方法:

// 设置非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

典型工作流:

epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, EPOLLIN); // 注册读事件

while (1) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (每个就绪事件) {
        if (事件 & EPOLLIN) {
            while ((len = recv(fd, buf)) > 0) {
                // 处理数据
            }
            if (len == -1 && errno == EAGAIN) break; // 数据读完
        }
        if (事件 & EPOLLOUT) {
            // 处理可写事件
        }
    }
}

关键差异对比:
在这里插入图片描述
边缘触发 (ET) 与 水平触发 (LT)
当使用 非阻塞 Socket + epoll 时,触发模式的选择至关重要:
在这里插入图片描述
ET 模式最佳实践:

// 边缘触发必须循环读取直到 EAGAIN
while (true) {
    ssize_t count = read(fd, buf, sizeof(buf));
    if (count == -1) {
        if (errno == EAGAIN) break; // 数据已读完
        // 处理真实错误
    }
    if (count == 0) { /* 连接关闭 */ }
    // 处理数据
}

在这里插入图片描述
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到