计算机网络 之 【高级IO】(Reactor模式设计)
目录1.Reactor模式设计诞生的原因2.Reactor 的定义3. 核心组件4. 与 epoll 的关系5.Reactor 的两种经典变体6.Reactor实现细节1.Reactor模式设计诞生的原因传统“每连接一线程”模型因线程栈内存暴涨与上下文切换开销在 C10K 场景下崩溃select/poll 虽然引入了多路复用但其全量描述符拷贝与 O(N) 内核遍历机制在高并发下 CPU 消耗线性增长epoll 通过红黑树与就绪队列解决了内核态的事件检测效率问题但将“检测到事件后如何组织业务逻辑”的复杂性抛给了应用层Reactor 正是为填补这一空白而抽象出的行为框架。它通过控制反转将“检测事件-分发事件-处理事件”的主循环固化为标准骨架开发者仅需向骨架中填充回调函数从而实现了 I/O 多路复用的逻辑复用与业务逻辑的彻底解耦2.Reactor 的定义Reactor是一种事件驱动的、用于同步非阻塞 I/O 多路复用的网络编程设计模式其核心思想是将“事件检测”与“事件处理”解耦使用一个事件循环EventLoop阻塞等待多路事件就绪然后将就绪的事件分发给对应的处理器Handler进行回调处理同步非阻塞 I/Oread 和 write 必须由用户进程自己调用同步但要求 fd 设置为 O_NONBLOCK没数据时立刻返回 EAGAIN 而不是卡死线程事件循环while (running) { int n epoll_wait(epfd, events, MAX_EVENTS, timeout); for (int i 0; i n; i) { Dispatch(events[i]); // 分发到对应 Handler } }分发与回调Reactor 本身不做业务逻辑它只负责事件分发Reactor 同步非阻塞 I/O 事件循环 分发回调3. 核心组件EventLoop::Run() │ ├─► n epoll_wait(epfd, events, ...); // 同步事件分离器 │ └─► for (i 0; i n; i) { // 事件分发器 fd events[i].data.fd; if (fd listenfd) { Acceptor::HandleRead(); // 接受新连接 } else { Connection::HandleRead(); // 处理已连接 Socket Connection::HandleWrite(); } }组件底层对应物核心职责关键成员 / 操作事件源文件描述符fd产生 I/O 事件的实体socket()、accept()返回的整数句柄同步事件分离器epoll_wait阻塞等待事件源就绪返回就绪事件数组int n epoll_wait(epfd, events, maxevents, timeout);事件分发器while循环 events遍历将就绪事件按类型路由至对应处理器for (int i 0; i n; i) { Dispatch(events[i]); }fd 到 Connection 的映射表std::unordered_mapint, std::shared_ptrConnection根据内核返回的fd快速定位应用层连接对象auto conn connections_.find(fd);Acceptorlisten_fd的EPOLLIN处理器接受新连接创建conn_fd及Connection对象并插入映射表int connfd accept(listenfd, ...);epoll_ctl(ADD, connfd, EPOLLIN);Connectionconn_fd的状态容器与应用层接口封装单个连接的全部状态、缓冲区与回调下方代码事件循环EventLoop驱动上述所有组件的无限循环体while (running) { ... }每个文件描述符必须关联一个独立的 Connection 结构体其中至少包含该 fd、用于处理 TCP 流式粘包半包的输入缓冲区、用于异步续传的输出缓冲区以及业务回调函数指针输入输出缓冲区应采用 std::vectorchar 而非 std::string因为前者明确表达“字节序列”的语义能安全容纳任意的二进制数据而不受 \0 截断或隐式 C 风格字符串转换的影响class Connection : public std::enable_shared_from_thisConnection { public: // 构造与析构 Connection(int fd, EventLoop* loop); ~Connection(); // 禁止拷贝 Connection(const Connection) delete; Connection operator(const Connection) delete; // I/O 事件处理由 EventLoop 回调 void HandleRead(); void HandleWrite(); void HandleClose(); // 业务层主动操作接口 void Send(const std::string data); void Send(const char* data, size_t len); void Shutdown(); // 设置业务回调 void SetMessageCallback(MessageCallback cb); void SetCloseCallback(CloseCallback cb); private: int fd_; // 连接的 Socket 句柄 EventLoop* loop_; // 所属事件循环 std::vectorchar inputBuffer_; // 输入缓冲区处理粘包/半包 std::string outputBuffer_; // 输出缓冲区待发送数据队列 // 业务回调函数 MessageCallback onMessage_; // 收到完整消息时的回调 CloseCallback onClose_; // 连接关闭时的回调 // 状态标志 bool reading_ true; bool writing_ false; };4. 与 epoll 的关系Reactor 是设计模式epoll 是 Linux 内核提供的实现该模式的底层系统调用一个典型的单线程 Reactor 骨架如下while (running) { // 1. 同步事件分离阻塞等待事件 int n epoll_wait(epfd, events, MAX_EVENTS, -1); // 2. 事件分发 for (int i 0; i n; i) { int fd events[i].data.fd; if (fd listenfd) { AcceptConnection(); // 处理器接受新连接 } else if (events[i].events EPOLLIN) { HandleRead(fd); // 处理器读取数据 } else if (events[i].events EPOLLOUT) { HandleWrite(fd); // 处理器发送数据 } } }5.Reactor 的两种经典变体单线程 Reactor 有个致命缺陷如果业务处理函数耗时太长整个循环就卡住了导致其他 10,000 个连接的读写事件无法被响应造成饥饿问题┌─────────────────────────────────────┐ │ 单一线程 │ │ epoll_wait → accept → read │ │ ↓ │ │ 业务处理可能阻塞 │ │ ↓ │ │ write │ └─────────────────────────────────────┘因此衍生出了两种主流变种A. 单 Reactor 多线程线程池技术┌─────────────────┐ ┌──────────────┐ │ Reactor 线程 │ ───► │ 线程池 │ │ - epoll_wait │ │ - 业务处理 │ │ - read/write │ ◄─── │ - 生成响应 │ └─────────────────┘ └──────────────┘架构EventLoop 只有一个只负责分发事件流程Connection 读到数据后立即把 buf 和 fd 打包成一个 Task扔给后端的线程池处理优势Reactor 线程永远轻快负责纯粹的 IO 读写。业务计算交给线程池阻塞执行挑战线程安全。多个线程可能同时想给同一个 fd 发送数据输出缓冲区需要加锁B.主从 Reactor 多线程┌─────────────────┐ │ Main Reactor │ ← 仅处理 accept │ (1 线程) │ └────────┬────────┘ │ 分发 conn_fd ┌────┴────┬────────┐ ↓ ↓ ↓ ┌────────┐ ┌────────┐ ┌────────┐ │ Sub │ │ Sub │ │ Sub │ ← 处理 read/write/业务 │Reactor │ │Reactor │ │Reactor │ │(线程1) │ │(线程2) │ │(线程N) │ └────────┘ └────────┘ └────────┘架构MainReactor1个 SubReactorN个通常等于 CPU 核数分工1MainReactor只负责 accept 新连接2拿到 client_fd 后通过轮询或哈希算法派发给某一个 SubReactor3SubReactor负责这个连接后续所有的读、写、关闭、异常处理优势极致性能。每个 SubReactor 跑在一个独立线程里天然无锁总结模式线程模型特点单 Reactor 单线程一个线程负责所有 accept、read、write、业务处理简单无锁但业务逻辑阻塞会影响所有连接如 Redis 6.0 之前单 Reactor 多线程Reactor 线程负责 I/O 事件分发业务逻辑提交给线程池处理解耦 I/O 与计算但 Reactor 本身仍是单点瓶颈主从 Reactor 多线程Main Reactor 只处理acceptSub Reactor 负责已连接 Socket 的 I/ONetty、Muduo、Nginx 的默认模型多核扩展性最佳单线程模型最简单但业务与 I/O 强耦合单 Reactor 多线程将计算卸载至线程池却保留了 I/O 单点主从 Reactor 则通过多级分发将 accept 与 I/O 彻底分离至不同线程组实现了 I/O 路径的完全无锁化与多核线性扩展6.Reactor实现细节1每个文件描述符关联一个独立的 Connection 结构体不同连接之间就不会相互干扰2在调用 epoll_ctl 将文件描述符注册到内核事件监听集合的同时必须在应用层建立该描述符到其专属 Connection 对象的映射关系进而快速定位并执行其私有读写操作3相关错误码错误码实际值含义触发场景正确处理方式EAGAIN11资源暂时不可用read时接收缓冲区空或write时发送缓冲区满静默返回等待下次epoll通知EWOULDBLOCK11与EAGAIN完全相同POSIX 标准允许混用Linux 内核实际返回EAGAIN同EAGAIN跨平台代码建议同时判断两者EINTR4系统调用被信号中断定时器触发、GDB 调试暂停、子进程状态变化必须重启系统调用不可视为错误退出4为了实现简单读写异常都统一到同一个函数中进行处理依次执行从 epoll 移除事件监听、关闭文件描述符、从映射表中删除对应 Connection 对象三个步骤5需通过周期性检测每个连接的上次活跃时间戳将超出空闲阈值的连接主动关闭并从 epoll 和映射表中移除以此保障服务端连接池的健康与可用性6TcpServer在初始化时预先设置一个业务回调函数当Connection从内核读取数据并解析出完整消息后通过该回调将数据向上传递给业务层处理从而实现网络I/O与业务逻辑的解耦Reactor模式正如打地鼠游戏EventLoop是同步阻塞在epoll_wait上的游戏面板Connection是每个独立监听的洞口内核通过中断机制通知“地鼠冒头”即事件就绪回调函数HandleRead/HandleWrite执行“打击动作”完成非阻塞I/O读写Reactor本身属于纯同步I/O模式所有读写操作均由主线程同步执行回调仅仅是事件分发后的函数调用机制若需实现“半同步半异步”则应将耗时业务逻辑交由线程池异步处理以释放主循环