从零实现高并发Web服务器:Reactor模型与epoll实战详解 📅 2026/7/21 6:54:56 1. 项目概述与核心价值最近在整理技术笔记翻到了几年前做的一个练手项目一个基于Reactor模型、用C实现的简易高并发Web服务器。当时为了吃透网络编程和多线程的配合没少折腾从select/poll一路踩坑到epoll最后用多线程Reactor跑了起来。现在回头看这个项目虽然代码量不大但把Linux下高性能网络服务的几个核心“钉子户”——I/O多路复用、事件驱动、线程池、非阻塞I/O——都串起来了特别适合想深入理解“服务器为什么能同时服务成千上万人”这个问题的朋友。这个项目能做什么简单说它就是一个能同时处理大量HTTP连接的微型服务器。你写个简单的HTML页面用浏览器或者压测工具比如ab、wrk去访问它它能扛住远高于传统“一个连接一个线程”模型的并发请求。它的核心价值不在于功能多全它只处理静态文件而在于架构清晰把Reactor模型从论文里搬到了可运行的代码中让你能亲手摸到事件循环、线程分工、连接管理的每一个齿轮是怎么咬合的。适合谁来参考如果你对网络编程有基础了解知道socket、TCP三次握手但对“高并发”具体怎么实现感到抽象或者你用过Nginx、Redis这些高性能中间件想看看它们底层的大致轮廓又或者你正在准备后端开发的面试被问到“epoll原理”、“Reactor和Proactor区别”时总觉得差点底气——那么这个项目的拆解应该能给你不少实实在在的料。我们不搞花架子就从最朴素的“一个连接一个线程”为什么不行开始一步步推到epoll多线程Reactor的完整实现。2. 从朴素模型到Reactor为什么我们需要事件驱动在动手写代码之前我们得先搞清楚要解决的核心矛盾是什么。假设我们要写一个最简单的Web服务器它的伪代码逻辑可能是这样的while (true) { int client_fd accept(server_fd, ...); // 阻塞等待新连接 std::thread handler_thread(handle_request, client_fd); // 为每个连接创建新线程 handler_thread.detach(); }这就是经典的“一个连接一个线程”Thread-Per-Connection模型。对于教学演示或者极低并发的场景它简单直接。但它的致命伤也显而易见线程资源消耗巨大。每个线程都需要独立的栈空间通常MB级别线程创建、销毁、上下文切换的开销在高并发下会成为不可承受之重。想象一下一万个并发连接就需要一万个线程大多数现代操作系统根本扛不住系统资源会迅速耗尽在调度线程上而不是处理实际业务。那么改进思路是什么核心在于让一个线程能够同时照看多个连接。这就引出了I/O多路复用技术。在Linux上我们经历了从select到poll再到epoll的演进。select/poll的问题在于它们每次调用都需要将用户态关心的所有文件描述符集合拷贝到内核态内核遍历这个集合来检查哪些描述符就绪然后再将整个集合拷贝回用户态。这个“全量拷贝线性遍历”的过程在连接数很多时比如成千上万效率会急剧下降。epoll的出现解决了这个瓶颈。它的核心机制是epoll_create在内核创建一个epoll实例返回一个文件描述符epfd。epoll_ctl向这个epoll实例epfd注册、修改或删除需要监控的文件描述符比如监听socket或已连接socket。这个过程是增量的不需要每次传递全部描述符。epoll_wait等待注册在epfd上的描述符产生I/O事件。它只返回已经就绪的事件列表而不是全部注册的描述符。这意味着应用程序无需遍历所有连接直接处理就绪事件即可。这种“事件通知”机制正是Reactor模式的基石。Reactor模式也叫反应器模式其核心思想是将I/O事件的检测与事件的处理进行解耦。一个或多个线程Reactor线程专门负责通过epoll_wait等系统调用监听所有连接的I/O事件如可读、可写。当某个连接的事件就绪时Reactor线程并不自己处理这个请求而是将这个“发生了什么事件、发生在哪个连接上”的信息封装成一个任务或事件对象分发给其他工作线程Worker Thread去执行具体的业务逻辑如读取HTTP请求、解析、构造响应、发送。这样做的好处是资源利用率高少数Reactor线程就能管理海量连接工作线程池的大小可以根据CPU核心数合理设置避免线程爆炸。职责清晰Reactor线程只负责高效的I/O事件分发是“快递分拣中心”工作线程负责计算密集或可能阻塞的业务处理是“包裹处理车间”。响应及时基于事件驱动有I/O事件发生才触发处理避免了轮询的空耗。我们项目要实现的正是这样一个“主从Reactor多线程”模型的简化版主线程作为主Reactor负责监听和接受新连接子线程作为工作线程组成线程池负责处理已连接socket的读写事件。3. 核心组件设计与实现拆解一个可运行的Reactor服务器需要几个核心组件协同工作。下面我们逐一拆解它们的设计思路和关键实现细节。3.1 事件循环EventLoopReactor的心脏事件循环是整个服务器的发动机它不断运转执行“等待事件 - 处理事件”的循环。在我们的设计中每个线程包括主线程和工作线程都可以拥有自己的EventLoop实例。关键数据结构epoll_fd_通过epoll_create1(EPOLL_CLOEXEC)创建的文件描述符。EPOLL_CLOEXEC标志很重要它表示这个文件描述符在exec系列函数执行时会被自动关闭防止泄漏到子进程。events_一个epoll_event数组作为epoll_wait的输出缓冲区存放一次调用返回的所有就绪事件。channel_map_一个映射表例如std::unordered_mapint, Channel*将文件描述符fd映射到其对应的Channel对象。这是高效事件分发的关键。核心方法loop()void EventLoop::loop() { while (!quit_) { int num_events epoll_wait(epoll_fd_, events_, MAX_EVENTS, timeout_ms); if (num_events 0) { // 处理错误通常EINTR信号中断可忽略 if (errno EINTR) continue; perror(epoll_wait error); break; } if (num_events 0) { // 超时可以处理一些定时任务 handleTimeout(); continue; } // 处理就绪事件 for (int i 0; i num_events; i) { int fd events_[i].data.fd; Channel* ch channel_map_[fd]; // 快速找到对应的Channel if (ch) { ch-set_revents(events_[i].events); // 设置当前触发的事件 ch-handleEvent(); // 调用Channel的事件处理回调 } } // 处理其他任务比如执行pending的异步回调 doPendingTasks(); } }注意事项线程安全性EventLoop的updateChannel、removeChannel等方法通常只应在拥有该EventLoop的线程中调用。如果其他线程需要操作比如新连接到来需要注册到工作线程的EventLoop需要通过队列等线程间通信机制将任务投递到EventLoop线程内执行这是常见的“one loop per thread”架构的通信方式。超时处理epoll_wait的最后一个参数是超时时间。设置为-1表示永久阻塞直到有事件发生设置为0表示立即返回用于非阻塞检查设置为正数则是毫秒级超时。合理的超时设置可以兼顾响应速度和CPU占用避免空转。我们可以在超时返回时num_events 0执行一些周期性的后台任务比如连接保活检查。事件集复用events_数组在每次epoll_wait返回后被复用。务必在每次循环中根据num_events来遍历不要依赖上一次循环残留的数据。3.2 通道Channel事件的封装与回调Channel类是对一个文件描述符fd及其相关事件的封装。它是连接底层epoll事件和上层业务逻辑的桥梁。每个需要被监听的fd如监听socket、客户端连接socket都会对应一个Channel对象。核心成员fd_它所管理的文件描述符。events_它关心的事件集合如EPOLLIN可读、EPOLLOUT可写、EPOLLET边缘触发。这个值会在调用epoll_ctl(EPOLL_CTL_ADD/MOD)时使用。revents_当前实际触发的事件集合由epoll_wait返回并设置。read_callback_,write_callback_,error_callback_事件触发时的回调函数。通常使用std::function绑定具体的处理函数。关键方法enableReading()/enableWriting()设置events_并调用EventLoop::updateChannel(this)将更新同步到epoll内核事件表。handleEvent()在EventLoop中当某个fd就绪后被调用。它会根据revents_判断发生了什么事件然后调用对应的回调函数。void Channel::handleEvent() { if ((revents_ EPOLLHUP) !(revents_ EPOLLIN)) { // 对端关闭连接且无数据可读 if (close_callback_) close_callback_(); return; } if (revents_ EPOLLERR) { if (error_callback_) error_callback_(); // 错误发生后通常也需要关闭连接 if (close_callback_) close_callback_(); return; } if (revents_ (EPOLLIN | EPOLLPRI | EPOLLRDHUP)) { // 可读事件或对端关闭连接有数据可读 if (read_callback_) read_callback_(); } if (revents_ EPOLLOUT) { // 可写事件 if (write_callback_) write_callback_(); } }实操心得边缘触发ET与水平触发LT的选择这是epoll使用中的一个关键决策点。EPOLLET标志表示边缘触发。水平触发LT默认只要文件描述符对应的读/写缓冲区非空/非满epoll_wait就会持续报告该事件。你可以在本次回调中不读完/写完所有数据下次循环它还会通知你。编程模型简单不容易遗漏事件但可能带来不必要的唤醒。边缘触发ET只有当文件描述符状态发生变化时比如从不可读变为可读或从不可写变为可写epoll_wait才会报告一次。这意味着一旦收到一个ET事件你必须循环读取或写入直到系统调用返回EAGAIN或EWOULDBLOCK表示资源暂时不可用确保缓冲区被清空或填满。ET模式效率更高减少了相同事件被重复通知的次数但编程复杂度高如果处理不当比如没读完全部数据会导致连接“饿死”后续数据到来但状态无变化不再通知。对于新手强烈建议先从LT模式开始。我们的第一个版本也使用LT因为它更安全、更直观。在确保LT模式工作稳定后可以尝试挑战ET模式那时你会对非阻塞I/O和缓冲区管理有更深的理解。3.3 线程池与任务队列工作的分发者工作线程池负责执行具体的业务逻辑。主Reactor主线程在accept新连接后需要将这个连接“分配”给线程池中的某个工作线程去监听其上的读写事件。设计要点线程池启动在服务器初始化时创建固定数量如CPU核心数的工作线程。每个工作线程运行自己的EventLoop::loop()进入事件循环等待任务。任务队列需要一个线程安全的队列用于存放待处理的新连接或者更抽象的任务。主线程将新连接的fd或封装好的Channel放入队列。任务分发策略工作线程如何从队列中取任务有两种常见模式全局队列竞争所有工作线程从一个全局任务队列争抢任务。实现简单但竞争可能成为瓶颈。轮询或哈希分配主线程通过简单的轮询Round-Robin或根据连接fd哈希直接将新连接分配给某个特定工作线程的私有队列。这减少了竞争是更常见的做法。在我们的简化实现中可以采用一个全局队列工作线程通过条件变量等待和获取任务。一个简单的线程池任务分发伪代码// 主线程Acceptor中 int client_fd accept(...); // 选择一个工作线程例如轮询 int index next_worker_index_ % worker_threads_.size(); worker_threads_[index]-addNewConnection(client_fd); // 在工作线程类中 void WorkerThread::addNewConnection(int fd) { { std::lock_guardstd::mutex lock(queue_mutex_); connection_queue_.push(fd); } condition_.notify_one(); // 通知工作线程的事件循环 } // 工作线程的EventLoop在超时或通过其他机制检查任务队列 void WorkerThread::handlePendingTasks() { std::vectorint new_fds; { std::lock_guardstd::mutex lock(queue_mutex_); while (!connection_queue_.empty()) { new_fds.push_back(connection_queue_.front()); connection_queue_.pop(); } } for (int fd : new_fds) { // 为这个fd创建Channel设置读回调并注册到本线程的EventLoop Channel* ch new Channel(loop_, fd); ch-setReadCallback(std::bind(HttpHandler::onRead, this, std::placeholders::_1)); ch-enableReading(); } }这里的关键是连接fd的读写事件监听从主线程“转移”到了被选中的工作线程的EventLoop上。后续这个连接的所有I/O事件都将由这个工作线程负责处理。3.4 连接管理与HTTP解析每个客户端连接对应一个Connection或HttpHandler类它持有socket fd管理连接的状态如是否正在关闭并负责HTTP协议的解析与响应生成。连接生命周期管理创建在accept后在工作线程中创建。数据读取在Channel的读回调中从socket循环读取数据非阻塞读直到EAGAIN并存入该连接的输入缓冲区。协议解析检查输入缓冲区尝试解析HTTP请求行、头部。这里需要处理不完整的请求数据还没收全这是网络编程的常态。可以使用状态机来解析。业务处理解析出完整的请求后根据方法GET/POST和路径生成响应内容。对于我们这个静态服务器就是根据路径读取本地文件。数据发送将响应内容写入连接的输出缓冲区。由于socket写缓冲区可能满一次write或send可能无法写完所有数据。此时需要监听EPOLLOUT事件在可写时继续发送直到输出缓冲区清空。发送完毕后要取消对EPOLLOUT的监听避免不必要的唤醒。关闭遇到错误、解析失败、收到Connection: close头部或完成响应后需要关闭时先关闭socket fd然后清理对应的Channel和Connection对象。务必记得从EventLoop的channel_map_中移除并调用epoll_ctl(EPOLL_CTL_DEL)这是防止内存泄漏和epoll监视异常的关键。HTTP解析注意事项缓冲区设计每个连接应有独立的输入/输出缓冲区。输入缓冲区建议使用可动态增长的容器如std::vectorchar或自己管理的char数组因为请求大小未知。非阻塞I/O循环无论是读还是写在LT模式下也建议使用循环配合非阻塞I/O直到返回EAGAIN这样可以一次性处理尽可能多的数据提高吞吐量。短连接与长连接HTTP/1.1默认是长连接Keep-Alive。这意味着一个TCP连接上可以传输多个HTTP请求/响应。服务器在发送完响应后不应立即关闭连接而是重置HTTP解析状态继续等待该连接上的下一个请求。这能显著减少TCP连接建立/关闭的开销。需要在解析头部时识别Connection字段并在响应头中正确设置Connection: keep-alive和Content-Length。4. 完整组装与运行流程现在我们把所有组件串联起来看看服务器从启动到处理一个请求的完整流程初始化创建主线程EventLoop主Reactor。创建监听socket绑定并监听端口如8080。创建Channel监听这个监听socket的EPOLLIN事件读回调设置为Acceptor::handleNewConnection。初始化线程池启动N个工作线程每个工作线程运行自己的EventLoop子Reactor。主循环启动主线程调用EventLoop::loop()。接受新连接客户端发起连接监听socket可读。主Reactor的epoll_wait返回触发监听socket对应Channel的read_callback_即Acceptor::handleNewConnection。在handleNewConnection中循环调用accept因为LT模式可能同时有多个连接到达直到返回EAGAIN。对每个接受的client_fd设置为非阻塞模式。通过轮询等策略选择一个工作线程将client_fd加入到该工作线程的任务队列并通知该工作线程。工作线程处理连接被选中的工作线程在其EventLoop循环中或通过条件变量唤醒获取到新的client_fd。为该client_fd创建Connection对象和Channel对象设置读回调为Connection::handleRead并将该Channel注册到本线程的EventLoop中监听EPOLLIN事件。处理HTTP请求客户端发送HTTP请求数据client_fd可读。工作线程的epoll_wait返回触发对应Channel的handleRead。handleRead中从socket读取数据到连接的输入缓冲区并尝试解析HTTP请求。如果解析出一个完整的请求则根据请求生成HTTP响应放入连接的输出缓冲区并调用Connection::sendResponse开始发送。sendResponse尝试直接写入socket。如果一次写完则发送完成如果只写了一部分返回EAGAIN则在该连接的Channel上启用EPOLLOUT监听等待下次可写时继续发送。发送响应与连接管理当socket可写时触发Channel的写回调继续发送输出缓冲区剩余数据。全部发送完成后如果是HTTP/1.1长连接则重置连接状态等待下一个请求如果是短连接或出错则调用Connection::handleClose。handleClose中调用epoll_ctl(EPOLL_CTL_DEL)删除对该fd的监听关闭socket fd并最终销毁Connection和Channel对象。这个流程清晰地展示了Reactor模型下事件如何被检测、分发和处理。主线程只负责“接客”把客人领进门accept后交给不同的“服务员”工作线程去服务。每个服务员同时服务多位客人多个连接但只在他们需要点菜可读或上菜可写时才去招呼。5. 性能调优与关键参数一个基础的Reactor服务器搭建起来后还可以从多个维度进行调优以适应更高的并发和吞吐量需求。5.1 系统级参数调整在Linux下一些系统参数会直接影响服务器的并发能力需要在部署前进行调整通常需要root权限参数配置文件路径说明与建议值文件描述符限制/etc/security/limits.conf每个进程能打开的最大文件数。一个连接就是一个fd。建议设置为65535或更高。ulimit -n可查看当前限制。端口范围与复用/proc/sys/net/ipv4/ip_local_port_range客户端连接使用的本地端口范围。一般不用改。TIME_WAIT状态/proc/sys/net/ipv4/tcp_tw_reuse/proc/sys/net/ipv4/tcp_tw_recycle(已废弃)服务器主动关闭连接后会进入TIME_WAIT。在高并发短连接场景下大量连接处于此状态会耗尽端口。设置tcp_tw_reuse 1允许复用处于TIME_WAIT的socket。注意tcp_tw_recycle在NAT环境下有问题内核4.1已移除不要使用。TCP快速打开/proc/sys/net/ipv4/tcp_fastopen允许在SYN包中携带数据减少一次RTT。可设置为3作为客户端和服务器都启用。Socket缓冲区大小代码中通过setsockopt设置SO_RCVBUF和SO_SNDBUF内核中用于收发数据的缓冲区大小。太小会影响吞吐量太大会占用过多内存。需要根据网络带宽和延迟来权衡。内核会自动在设定值和两倍值之间调整通常不设或设为0使用系统默认值即可。重要提示修改系统参数需谨慎尤其是在生产环境。建议先在测试环境验证。5.2 服务器核心参数在我们的服务器代码中以下几个参数对性能有直接影响工作线程数量这不是越多越好。过多的线程会导致大量的上下文切换开销。一个经典的设置是CPU核心数 1或CPU核心数 * 2。对于I/O密集型如Web服务器且使用了异步I/O线程数可以略多于核心数以在某个线程因系统调用轻微阻塞时其他线程能继续利用CPU。可以通过压测找到最佳值。epoll_wait超时时间在EventLoop::loop()中。如果设置为-1永久阻塞那么当没有I/O事件时所有工作线程都会休眠不消耗CPU。这是最节能的方式。如果设置为一个较小的正数如10ms那么线程会更频繁地被唤醒可以更及时地处理一些非I/O的异步任务比如定时器、线程池队列中的任务但会增加一些CPU空转。需要根据业务特点权衡。连接读写缓冲区大小每个Connection对象内部的输入/输出缓冲区初始大小和扩容策略。初始大小太小会导致频繁扩容内存分配和拷贝太大则浪费内存。对于典型的HTTP请求初始缓冲区设为4KB或8KB是个不错的起点。扩容策略可以采用翻倍增长以减少分配次数。5.3 内存与对象池在高并发下频繁地创建和销毁Connection和Channel对象会导致大量的内存分配和释放可能引发内存碎片并增加垃圾回收如果使用带GC的语言或析构开销。一个常见的优化是使用对象池。简易对象池思路在服务器启动时预先分配一定数量的Connection对象放入一个空闲链表。当新连接到来时从空闲链表中取出一个对象初始化其fd和状态后使用。当连接关闭时重置该对象状态将其放回空闲链表而不是直接delete。当空闲链表为空时再动态创建新的对象当空闲对象过多时可以释放一部分。这样可以大大减少动态内存管理的开销。但实现时需要注意线程安全以及对象重置必须彻底避免残留上一个连接的数据。6. 常见问题排查与调试技巧开发过程中你肯定会遇到各种问题。下面是一些典型场景和排查思路。6.1 连接数上不去报“Too many open files”这是最经典的问题说明进程打开的文件描述符达到了系统或用户限制。排查步骤ulimit -n查看当前shell的文件描述符限制。这只是一个会话限制。cat /proc/pid/limits查看你服务器进程实际生效的限制pid替换为你的进程ID。重点看Max open files这一行。在代码中每次accept、socket、epoll_create等系统调用后都要检查返回值。如果返回-1打印errno用strerror(errno)或perror。如果errno是EMFILE或ENFILE就说明达到了限制。确保你的服务器在关闭连接时正确调用了close(fd)并且从epoll实例中删除了监控EPOLL_CTL_DEL。资源泄漏是导致fd耗尽的元凶。调试技巧写一个简单的脚本每隔一秒打印一下进程的fd数量ls -l /proc/pid/fd | wc -l。观察在压力测试下fd数量是否持续增长而不下降。如果是肯定有泄漏。6.2 服务器CPU占用率100%但吞吐量很低这可能是因为陷入了“忙等待”busy-loop。可能原因及排查某个连接上的事件处理不完特别是在边缘触发ET模式下如果可读事件触发后你没有循环读到EAGAIN就返回了那么只要对方还有数据发送这个fd的状态就不会再变化一直可读epoll_wait就不会再报告它导致数据积压。但你的应用层可能还在别处空转。对于ET模式读必须读到EAGAIN写必须写到EAGAIN这是铁律。事件回调函数中有阻塞操作比如在read_callback_里进行了同步的数据库查询或磁盘IO。这会阻塞整个EventLoop导致其他连接的事件得不到及时处理。在Reactor线程EventLoop所在线程中绝对不能有阻塞操作。耗时的任务必须丢到独立的业务线程池或使用异步IO。epoll_wait超时时间设置为0这会导致epoll_wait立即返回即使没有事件线程也会空转消耗CPU。检查你的timeout_ms参数。调试技巧使用perf top或htop查看是哪个函数占用CPU高。在代码关键路径加日志看看事件循环的频率是否异常的高。6.3 压测时出现“Address already in use”或连接失败服务器重启后监听端口无法立即绑定。原因服务器主动关闭连接后连接会进入TIME_WAIT状态持续2MSL一般是60秒。在此期间这个四元组源IP、源端口、目的IP、目的端口是被占用的。如果服务器重启过快试图绑定相同的IP和端口就会失败。解决方案在创建监听socket后设置SO_REUSEADDR选项。int yes 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, yes, sizeof(yes)) 0) { perror(setsockopt SO_REUSEADDR failed); // 处理错误 }这个选项允许绑定处于TIME_WAIT状态的地址是服务器程序的标配。更激进地可以设置SO_REUSEPORTLinux 3.9它允许多个socket绑定到相同的IP和端口内核会进行负载均衡。这对于多进程服务器模型很有用。6.4 内存缓慢增长或泄漏排查手段使用Valgrind这是最强大的内存调试工具。用valgrind --leak-checkfull ./your_server运行你的程序结束后会给出详细的内存泄漏报告。注意Valgrind会显著降低程序运行速度只适合在测试环境使用。代码审查重点检查所有new/malloc的地方是否有对应的delete/free。特别是在异常处理路径和连接关闭路径上资源释放的代码是否都能执行到。对象生命周期管理在Reactor模型中一个常见的错误是Channel或Connection对象被提前销毁但它的文件描述符还在被epoll监控。当事件触发时EventLoop会通过fd去channel_map_里找Channel指针如果该指针已失效就会导致段错误Segmentation Fault。确保销毁对象的顺序先epoll_ctl(EPOLL_CTL_DEL)再close(fd)最后销毁对象。6.5 使用调试工具观察服务器状态netstat/ss查看服务器监听状态和活跃连接。ss -tlnp | grep :8080查看谁在监听8080端口。ss -tan | grep :8080查看所有与8080端口相关的TCP连接状态ESTAB, TIME-WAIT等。lsof列出进程打开的文件。lsof -p pid查看你的服务器进程打开了哪些文件包括socket。strace跟踪系统调用。可以看你的程序在做什么。strace -p pid跟踪一个正在运行的进程。strace -e poll,epoll_wait,accept,read,write ./your_server在启动时跟踪特定的系统调用对于理解程序行为非常有帮助。7. 从Reactor到Proactor异步I/O的展望我们实现的基于epoll的Reactor模型本质上是同步非阻塞I/O。线程通过epoll得知某个fd可读/可写但实际的read/write系统调用还是由这个线程同步发起的这个调用可能因为缓冲区等原因而阻塞尽管时间很短。更进一步的模型是Proactor前摄器模型它实现了真正的异步I/O。在Proactor中应用程序发起一个I/O操作如异步读aio_read后立即返回操作系统负责完成整个I/O操作将数据从网卡读到应用指定的缓冲区操作完成后通过某种方式如信号、完成端口IOCP、事件通知通知应用程序。此时数据已经准备好应用程序直接使用即可。Linux上原生的异步I/OAIO接口aio_*对文件支持较好但对网络socket的支持一直不完善。Windows的IOCPI/O Completion Ports是经典的Proactor实现。在Linux上通常使用io_uringLinux 5.1引入来构建高性能的Proactor服务器。io_uring通过两个共享的环形队列提交队列SQ和完成队列CQ来批量提交和收割I/O操作避免了多次系统调用性能极高。对于我们这个项目理解Reactor已经足够应对大多数场景。但知道Proactor和io_uring的存在能让你明白技术演进的路径。当你的Reactor服务器遇到性能瓶颈并且你确信瓶颈在于I/O系统调用本身时就该考虑向异步I/O模型探索了。最后这个epoll多线程的Reactor Web服务器虽然只有几百行核心代码但它像一张地图带你穿越了高性能网络编程最崎岖的地带。亲手实现一遍再对比Nginx、Redis等顶级开源项目的网络模块设计它们多是多Reactor或多进程变种你会对“高并发”这三个字有完全不同的、具象化的理解。所有的优化、所有的设计模式最终都是为了更高效地管理计算机那点最宝贵的资源CPU时间、内存和I/O通道。