Reactor 模式
前面把 I/O 多路复用、非阻塞、epoll 这些概念理顺之后,再往下看服务端代码怎么组织时,很快就会碰到一个绕不开的词:
- Reactor
这个词如果只看定义,其实很容易觉得有点虚。因为它既不是具体的系统调用,也不是某个固定函数名,而更像是一种写服务器的方式。
我一开始最容易混的点,是把:
epoll- 事件循环
- 线程池
- Reactor
全部揉成一团。
后来慢慢顺下来之后,我现在更愿意先把最核心的话立住:
Reactor 模式的本质,就是由一个事件循环统一等待 I/O 事件,哪个 fd 就绪了,就把它分发给对应的处理器去处理。
也就是说,它关心的不是“内核怎么监听 fd”,而是:
当
epoll_wait()把一批 ready fd 返回给我之后,我的程序整体应该怎么组织。
一、为什么会有 Reactor
如果只看最朴素的服务端写法,最容易想到的是:
1. 一个连接一个线程
每个线程负责:
read()- 处理业务
write()
这种写法很好理解,但问题也很明显:
- 连接一多,线程数就会爆炸
- 大量线程其实都在等 I/O
- 上下文切换和内存占用都很大
2. 一个线程手动轮询所有连接
这能省线程,但如果没有事件时还在不停轮询,也会浪费 CPU。
3. 后来有了 I/O 多路复用
select、poll、epoll 解决的是:
怎么高效地等待很多 fd。
但多路复用只解决了“谁就绪了”这个问题,并没有告诉你:
- 新连接谁来接
- 已连接谁来读
- 业务谁来算
- 响应谁来发
- 整个服务端代码怎么组织
Reactor 模式就是在回答这件事。
二、Reactor 到底是什么
我现在更愿意把 Reactor 理解成:
一个围绕事件循环展开的调度中心。
它自己通常不直接执行业务逻辑,而是负责:
- 等事件
- 分发事件
- 调对应的 handler 去处理
所以如果把它说得接地气一点,它更像是一个“统一前台”:
- 谁来了新连接
- 谁现在可读
- 谁现在可写
- 谁该关闭了
它先把这些事分发出去,再由具体 handler 去干活。
三、Reactor 和 epoll 到底是什么关系
这个地方一定要先分清。
1. epoll 是机制
epoll 解决的是:
- 怎么监听很多 fd
- 怎么拿到就绪事件
它只是一个 I/O 多路复用工具。
2. Reactor 是模式
Reactor 解决的是:
- 拿到这些 ready 事件后,应用层代码怎么组织
所以更准确地说:
epoll是工具,Reactor 是基于epoll这种工具搭出来的一种事件驱动架构模式。
四、Reactor 模式里的几个核心角色
Reactor 这一块如果完全抽象讲,容易发飘。把几个角色拆开之后就顺很多。
1. Reactor / Event Loop
这是核心。
它通常负责:
- 调
select/poll/epoll_wait - 等待一批 fd 的就绪事件
- 拿到 ready list
- 依次分发给对应 handler
所以它更像是:
事件循环 + 分发器。
2. Acceptor
这是专门处理“新连接到来”的部分。
当监听 socket 可读时,通常意味着:
有新的连接可以
accept()。
这时通常就由 Acceptor 去:
accept()新连接- 拿到新的 conn_fd
- 配置成非阻塞
- 注册到 Reactor 中
3. Handler
Handler 负责处理具体连接的 I/O 事件。
例如某个连接可读时,handler 通常会:
read()- 解析请求
- 处理连接状态
- 决定是否需要回写
某个连接可写时,handler 通常会:
write()- 处理写半包
- 如果写完了,取消写事件关注
所以更准确地说:
一个连接通常都会对应一套状态和 handler。
4. 业务线程池(可选)
如果业务处理比较重,例如:
- 查数据库
- 调 RPC
- 做复杂计算
通常就不会在 Reactor 线程里直接做完,而是把任务丢给 worker 线程池。
否则 I/O 线程很容易被业务逻辑拖慢。
五、Reactor 的基本工作流程
这一部分如果只看概念,很容易空。按一个 TCP 服务器从头跑一遍会更清楚。
1. 先创建监听 socket
服务端先:
socket()bind()listen()
然后把监听 fd 设成非阻塞,并注册到 epoll。
2. Reactor 线程进入等待
事件循环线程会阻塞在:
epoll_wait(...)这里没有问题,这正是它高效等待事件的方式。
3. 来了新连接
当有客户端建连时,监听 socket 变成可读。
epoll_wait() 返回后,Reactor 发现:
- 这是监听 fd
- 说明现在该做的是
accept()
于是 Acceptor 去:
accept()新连接- 拿到 conn_fd
- 设成非阻塞
- 注册读事件
4. 某个连接收到数据
之后如果某个 conn_fd 可读,epoll_wait() 会再次返回。Reactor 把这个事件交给对应 handler,handler 通常会:
- 非阻塞
read() - 尽量把当前能读的数据读完
- 解析协议
- 决定后续怎么处理
5. 业务处理
如果业务逻辑很轻,可以直接在当前线程里做。
如果业务逻辑重,一般会把请求交给线程池。
6. 回写响应
如果需要回数据,通常不是一直监听写事件,而是:
- 先把响应放进连接的发送缓冲区
- 在确实有待发送数据时再关注可写事件
- socket 可写时,handler 再去
write() - 如果没写完,就保留剩余数据,下一次继续写
- 如果写完了,就取消对写事件的关注
7. 关闭连接
如果对端关闭、协议错误,或者业务决定断开连接,就:
- 从 Reactor 注销 fd
close(fd)- 清理连接状态
六、为什么 Reactor 通常要配合非阻塞
这个点非常关键。
Reactor 的事件循环线程最怕的事情就是:
在处理某一个 fd 时,被这个 fd 阻塞住。
假设一次 epoll_wait() 返回了很多 ready fd,如果处理第一个 fd 时用了阻塞式 read(),而且又想一次把请求读完整,就可能发生:
- 当前只有一部分数据到了
- 后面的数据还没到
- 线程卡在这个阻塞式
read()上
这样后面的 ready fd 就全处理不了了。
所以 Reactor 模式下通常都要求:
- socket 非阻塞
- 读尽量读到
EAGAIN - 写尽量写到
EAGAIN
非阻塞的意义不是“更快”,而是:
不能让某一个连接把整个事件循环拖死。
七、单 Reactor 单线程
这是最简单的一种 Reactor 结构。
结构
- 一个 Reactor 线程
- 同时负责:
epoll_waitacceptread- 业务处理
write
优点
- 模型最简单
- 没有线程竞争
- 好理解、好实现
缺点
- 业务一重就容易拖慢整个事件循环
- 一个线程既管 I/O 又管业务,容易成为瓶颈
典型印象
经典 Redis 很有这种味道,虽然它还有自己的一些细节实现。
八、单 Reactor 多线程
这是工程里非常常见的一种过渡模型。
结构
- 一个 Reactor 线程负责:
epoll_waitacceptread/write
- 业务逻辑交给 worker 线程池
这时业务线程一般做什么
业务线程通常只负责:
- 处理业务逻辑
- 组装响应结果
而真正的 socket 发送,一般还是交回 Reactor 线程去做。
更准确地说:
Reactor 线程统一管连接的 I/O,业务线程只产出结果,不直接长期接管 socket。
这样做的原因主要有几个:
- 避免多个线程并发操作同一个 socket
- 更方便处理写半包和
EAGAIN - 发送事件的注册和取消由 Reactor 统一维护更自然
优点
- I/O 线程可以更专注网络事件
- 业务计算不会直接堵住事件循环
缺点
- 线程协作和连接状态管理会复杂一些
九、主从 Reactor 多线程
这是高并发服务器里非常经典的一种结构。
结构
- 主 Reactor:只负责监听 socket、接收新连接
- 子 Reactor:各自负责一批已建立连接的读写事件
- 业务线程池:负责重业务逻辑(可选)
工作方式
- 主 Reactor 收到新连接
accept()拿到 conn_fd- 把 conn_fd 分配给某个子 Reactor
- 之后这个连接的读、写、关闭通常都由对应子 Reactor 负责
- 如果有业务线程池,业务做完后一般也是交回这个连接所属的子 Reactor 发送响应,而不是交回主 Reactor
所以从职责上看:
- 主 Reactor 更像“接线员”
- 子 Reactor 更像“真正管连接的人”
优点
- 接入和已连接 I/O 分工更清晰
- 更容易水平扩展
- 更适合连接很多的场景
缺点
- 模型更复杂
- 线程之间的连接分配、唤醒和状态协作会更麻烦
十、Reactor 和 Proactor 的区别
这两个名字经常一起出现,最好顺手分清。
Reactor
流程是:
- 内核告诉你“这个 fd 就绪了”
- 你自己去
read/write - 你自己完成真正的数据处理
也就是说,它是:
就绪通知模型。
Proactor
流程更像:
- 你把异步 I/O 请求先交给内核
- 内核把读写都做完
- 完成后再通知你
也就是说,它是:
完成通知模型。
而我们平时基于 epoll 这一套讲 Reactor,本质上还是:
内核只告诉我 ready,我自己去把 I/O 收尾。
十一、Reactor 最容易踩的几个坑
1. 把重业务直接放进 Reactor 线程
如果 I/O 线程里直接做慢 SQL、远程调用、大量序列化,事件循环吞吐会明显下降。
2. 用阻塞式 read/write
这会直接把事件循环拖住。
3. ET 模式下没读到 EAGAIN
如果 ET 模式下没把数据一次性读干净,后面可能不会再次提醒。
4. 一直监听写事件
很多 socket 大部分时间本来就可写,如果一直监听写事件,很容易被无意义可写通知刷屏。
所以一般是:
- 真有待发送数据时才关心可写
- 写完就取消
5. 没有连接状态机
真正的 Reactor 服务器不是“事件来了就 read() 一下”这么简单,通常还要维护:
- 读缓冲
- 写缓冲
- 当前解析状态
- 半包 / 粘包状态
- 连接关闭状态
所以本质上它往往是:
事件驱动 + 连接状态机。
十二、小结
把这一块重新收一下,我现在更愿意这样理解:
Reactor 模式本质上是把
select/poll/epoll这种 I/O 多路复用机制,组织成一套事件驱动的服务端结构:由事件循环统一等待很多 fd 的就绪事件,一旦某个 fd 可读、可写或者有新连接到来,就把事件分发给对应的 handler 去处理。它解决的不是“内核怎么监听 fd”,而是“应用程序在拿到 ready 事件之后,整体代码怎么组织”。在这种模式下,通常会配合非阻塞 socket 使用,避免某一个连接拖住整个事件循环。根据线程划分方式不同,常见又可以分成单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程几种结构;如果再和 Proactor 对比,可以理解成 Reactor 是“就绪后我自己处理”,Proactor 是“完成后再通知我”。把这些层次分开之后,Reactor 模式就不会再显得很虚。
