目录
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/O:read 和 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_map<int, std::shared_ptr<Connection>> | 根据内核返回的fd快速定位应用层连接对象 | auto conn = connections_.find(fd); |
| Acceptor | listen_fd的EPOLLIN处理器 | 接受新连接,创建conn_fd及Connection对象,并插入映射表 | int connfd = accept(listenfd, ...);epoll_ctl(ADD, connfd, EPOLLIN); |
| Connection | conn_fd的状态容器与应用层接口 | 封装单个连接的全部状态、缓冲区与回调 | 下方代码 |
| 事件循环 | EventLoop | 驱动上述所有组件的无限循环体 | while (running) { ... } |
每个文件描述符必须关联一个独立的 Connection 结构体,其中至少包含该 fd、用于处理 TCP 流式粘包半包的输入缓冲区、用于异步续传的输出缓冲区以及业务回调函数指针;输入输出缓冲区应采用 std::vector<char> 而非 std::string,因为前者明确表达“字节序列”的语义,能安全容纳任意的二进制数据而不受 \0 截断或隐式 C 风格字符串转换的影响
class Connection : public std::enable_shared_from_this<Connection> { 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::vector<char> 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) │ └────────┘ └────────┘ └────────┘- 架构:MainReactor(1个)+ SubReactor(N个,通常等于 CPU 核数)
- 分工:(1)MainReactor:只负责 accept 新连接(2)拿到 client_fd 后,通过轮询或哈希算法,派发给某一个 SubReactor(3)SubReactor:负责这个连接后续所有的读、写、关闭、异常处理
- 优势:极致性能。每个 SubReactor 跑在一个独立线程里,天然无锁
总结:
| 模式 | 线程模型 | 特点 |
|---|---|---|
| 单 Reactor 单线程 | 一个线程负责所有 accept、read、write、业务处理 | 简单无锁,但业务逻辑阻塞会影响所有连接(如 Redis 6.0 之前) |
| 单 Reactor 多线程 | Reactor 线程负责 I/O 事件分发,业务逻辑提交给线程池处理 | 解耦 I/O 与计算,但 Reactor 本身仍是单点瓶颈 |
| 主从 Reactor 多线程 | Main Reactor 只处理accept,Sub Reactor 负责已连接 Socket 的 I/O | Netty、Muduo、Nginx 的默认模型,多核扩展性最佳 |
- 单线程模型最简单但业务与 I/O 强耦合
- 单 Reactor 多线程将计算卸载至线程池却保留了 I/O 单点
- 主从 Reactor 则通过多级分发将 accept 与 I/O 彻底分离至不同线程组,实现了 I/O 路径的完全无锁化与多核线性扩展
6.Reactor实现细节
(1)每个文件描述符关联一个独立的 Connection 结构体,不同连接之间就不会相互干扰
(2)在调用 epoll_ctl 将文件描述符注册到内核事件监听集合的同时,必须在应用层建立该描述符到其专属 Connection 对象的映射关系,进而快速定位并执行其私有读写操作
(3)相关错误码
| 错误码 | 实际值 | 含义 | 触发场景 | 正确处理方式 |
|---|---|---|---|---|
EAGAIN | 11 | 资源暂时不可用 | read时接收缓冲区空,或write时发送缓冲区满 | 静默返回,等待下次epoll通知 |
EWOULDBLOCK | 11 | 与EAGAIN完全相同 | POSIX 标准允许混用,Linux 内核实际返回EAGAIN | 同EAGAIN,跨平台代码建议同时判断两者 |
EINTR | 4 | 系统调用被信号中断 | 定时器触发、GDB 调试暂停、子进程状态变化 | 必须重启系统调用,不可视为错误退出 |
(4)为了实现简单,读写异常都统一到同一个函数中进行处理(依次执行从 epoll 移除事件监听、关闭文件描述符、从映射表中删除对应 Connection 对象三个步骤)
(5)需通过周期性检测每个连接的上次活跃时间戳,将超出空闲阈值的连接主动关闭并从 epoll 和映射表中移除,以此保障服务端连接池的健康与可用性
(6)TcpServer在初始化时预先设置一个业务回调函数,当Connection从内核读取数据并解析出完整消息后,通过该回调将数据向上传递给业务层处理,从而实现网络I/O与业务逻辑的解耦
Reactor模式正如打地鼠游戏:EventLoop是同步阻塞在epoll_wait上的游戏面板,Connection是每个独立监听的洞口,内核通过中断机制通知“地鼠冒头”即事件就绪,回调函数HandleRead/HandleWrite执行“打击动作”完成非阻塞I/O读写;
Reactor本身属于纯同步I/O模式,所有读写操作均由主线程同步执行,回调仅仅是事件分发后的函数调用机制
若需实现“半同步半异步”,则应将耗时业务逻辑交由线程池异步处理以释放主循环