Skip to content

Reactor 网络模型:拆解 5 个参与者,就真正读懂了高并发

背景:为什么要理解 Reactor 网络模型

一谈到 Reactor,想必很多人都认为这是一个使用了 epoll 的高并发网络模型。然而大部分人对 Reactor 内部是怎么处理的、背后涉及的原理却一知半解。只有对 Reactor 背后的原理了解后,才能对 Reactor 不断演化的网络框架有个系统性的认识。

这篇文章就来解答你的疑虑,将 Reactor 背后的原理一一进行拆解。读完,我相信你对 Reactor 高并发网络模型会有不一样的认识

文章先拆解 Reactor 的 5 个核心角色;再把它们连点成线,串成一条完整的链路。让我们开始吧

一、Reactor 网络模型解决的核心问题

传统阻塞式 I/O 主要的问题是什么?

每个客户端连接对应一个线程:这是最简单的一种网络模型,也就是每来一个连接就创建一个线程处理它。初次接触网络编程时,都会接触到。

这个模型很好理解:

  • 客户端连接 1 → 线程 1;
  • 客户端连接 2 → 线程 2;
  • ……
  • 客户端连接 N → 线程 N。

每个线程里大概按 acceptread → 处理业务 → writeclose 的顺序执行。

然而每个线程都直接进行 acceptreadwrite 操作,这些系统调用都可能阻塞。假设一个客户端连接建立后迟迟不发送数据,那么这个线程就会一直阻塞在 read 上,此时这个线程也就无法处理别的客户端请求。

这种网络模型并发数取决于线程的个数。连接数少的时候,性能差异还不明显。可生产环境下,单机 QPS 并发请求数通常都在 5 万以上,因此要想达到这种并发数基本上不可能。一旦连接数上来以后,问题就非常明显了:

  • 每个连接占用一个线程,线程数量会迅速膨胀;
  • 每个线程都有栈空间,内存消耗很高;
  • 大量线程之间频繁切换,CPU 时间浪费在调度上;
  • 业务高峰时,系统可能不是被计算压垮,而是被线程管理拖垮。

因此,在生产环境下,我想没有哪个企业会使用来一个客户端连接就创建一个线程这种网络模型,这对于高并发服务器来说是灾难性的。

高并发服务器真正要解决的问题不是"如何创建更多线程来处理客户端请求",而是"如何用更少的线程来处理更多的连接"。这才是 Reactor 模型发挥作用的原因。

那使用线程池能解决这个问题吗?

答案是: 没从根本上解决问题

线程池相比"来一个客户端连接就创建一个线程"确实改善了一些,但它还是没有从根本上解决 I/O 阻塞问题。

因为一连接一线程的问题是连接数越多、线程数越多;而线程池的改进是提前创建固定数量的线程,让所有的客户端连接复用这些线程,它的模型大概是这样:

线程池模型

这种网络模型,通常会有一个专门的接收线程负责 accept,处理客户端连接。并将连接后的 fd 放到任务队列中,然后发送事件通知,唤醒线程池中的线程,从任务队列取数据。

线程池从任务队列中取出 fd 后,就可以对这个 fd 进行数据读写。

使用线程池后,解决了"线程创建和销毁成本太高"的问题。然而并没有解决"线程阻塞等待 I/O"的问题。如果工作线程拿到一个连接 fd 后,此时客户端依旧不发数据,线程仍然会阻塞在 read 等待客户端发数据,线程无法再处理其他请求。

因此线程池模型在高并发场景下仍然有几个问题:

  • 工作线程数量有限,请求多了以后任务会堆积在队列里;
  • 线程虽然复用了,但阻塞等待 I/O 的问题还在;
  • 线程池规模太小会排队,规模太大又会带来上下文切换和内存压力。

也就是说,线程池它没有改变阻塞 I/O 的问题。因此需要有一种方式,不再让每个线程阻塞在 read/write 等系统调用上,而是让线程集中等待大量客户端连接的 I/O 就绪事件,这就引出了 I/O 多路复用。

Reactor 的基本思想是什么?

统一等待、集中分发:Reactor 的核心思想是,每个连接不再单独阻塞在 read/write 等系统调用上,而是阻塞在 epoll 等 I/O 多路复用上。

把多个客户端连接的读写事件统一注册到一个事件分发器中(例如 epoll),由它集中等待事件发生,再把就绪事件分发给对应的事件处理函数。

换句话说,Reactor 把网络处理拆成两件事:

  • 等待 I/O 就绪:交给统一的事件循环;
  • 处理 I/O 事件:交给对应的事件处理函数。

因此,它的整体流程可以概括为:

  1. 注册 fd 关注的读写事件;
  2. 等待读写事件的发生;
  3. epoll 发现就绪事件,并把就绪事件返回给事件循环;
  4. 事件循环分发事件,调用对应 fd 的事件处理函数;
  5. 回到事件循环,继续 epoll_wait 等待事件的发生

我们来看下这个事件循环

事件循环

这里最重要的是事件循环,可以把事件循环理解为承担调度器的角色。它是 Reactor 线程的主循环。平时就在 epoll_wait 系统调用中睡眠等待,当有事件发生时就从睡眠中返回,然后根据fd查找对应的事件处理函数,进行事件分发,最终调用事件处理函数。

Reactor 不是处理完一次请求就结束了,而是不断重复"等待事件"和"处理事件"。每一次循环只处理已经就绪的连接,未就绪的事件,内核根本就不会触发。

这里最重要的变化是:线程不再阻塞在每个 read/write 系统调用上,而是阻塞在 I/O 多路复用上。没有事件发生时,线程睡眠等待。一旦连接有事件发生时,线程被唤醒,只处理那些真正就绪的 fd。

这也是 Reactor 网络模型要解决的核心问题

很多人认为,这不就是 I/O 多路复用吗?和 Reactor 有什么关系

是的,这是 I/O 多路复用,但 I/O 多路复用只是其中的一部分。

不管是 select/epoll 等,都是 I/O 多路复用,让本来阻塞在 read/write 系统调用的线程,统一的阻塞在 epoll 等 I/O 多路复用上。

单单 I/O 多路复用还不够,这仅仅是食材,关键在于怎么利用这个食材做出一道美味佳肴来。因此在 I/O 多路复用基础上,还需要事件注册/卸载、事件循环、事件分发、事件处理函数等其他模块的配合,共同构建一个高并发服务器。

到此可以看出,Reactor 就是一个网络框架,或者说是一种设计模式。内部使用了 epoll 等 I/O 多路复用,来搭建出一套高并发服务器。

epoll 只是 Reactor 中的其中一个参与者,Reactor 中一共由 5 个参与者组成。我们一一来看下。

二、Reactor 模式的 5 个核心参与者

前面已经提到,Reactor 网络模型的核心思想是"统一等待、集中分发":线程不再阻塞在每个 read/write 系统调用上,而是阻塞在 I/O 多路复用上。有事件发生时就进行分发。

想要真正去对 Reactor 框架有个系统性的认识,有 5 个问题需要面对。先从第一个问题开始说起。

操作系统通知你 fd=8 可读,那你是怎么知道它是谁呢?

要知道 Reactor 面对的可不只是一个连接,而是成千上万个连接。当操作系统通知事件分发器(epoll)"有一个事件发生了",如果没有一个明确的身份标识,Reactor 根本分不清这个 fd 是谁:

  • 是监听 socket 有了新的客户端连接?
  • 或者是某个客户端发来了数据?
  • 或者是某个客户端连接触发了写事件,可以继续发送数据了?
  • 或者某个定时器超时了,需要调用定时器回调函数?

这个身份标识,在 Reactor 模型里叫 Handle。也就是 Linux 网络编程中的文件描述符 fd。常见的 Handle 有这么几类:

  • listenfd:监听 socket 的文件描述符;
  • connfd:已连接 socket 的文件描述符;
  • timerfd:定时器文件描述符;
  • eventfd:事件通知文件描述符。

注意: Handle 本身并不处理事件,它只负责标识事件源。

当操作系统告诉 Reactor fd = 8 可读时,Reactor 就知道 8 号连接有数据可以读了,再根据 fd = 8 找到对应的处理函数。如果没有这个标识,fd 和对应的事件处理函数就无法关联上,那对事件的分发也就无从谈起。

Handle(文件描述符)

关键点是:Handle 绝不会处理具体的事件,它只回答一个问题, 事件源是谁?

一万个连接,难道要一个个去问吗?

已经知道了是哪个事件源产生了事件,这还不够。关键得让 Reactor 知道是哪个事件源发生了事件才行。 如果还要 Reactor 挨个去问是哪个事件源就绪了,那和轮询方式没有任何区别,效率很低。

真正需要的是一种机制:一次性把所有 Handle,也就是 fd 注册进去。然后一次调用就知道"这些 Handle 里面有哪些是已经就绪了"。这就是 Synchronous Event Demultiplexer(同步事件多路分离器)要解决的问题。

这是在 ACE 中专门定义的术语,名字读起来有点绕口,其实在操作系统层面,它通常就是 I/O 多路复用:

  • select
  • poll
  • epoll

比如 Reactor 把 10000 个连接 fd 都注册进去,然后调用一次 epoll_wait;如果其中 20 个连接有数据可读,epoll_wait 就一次性返回这 20 个就绪事件,例如:

  • fd = 3,可读;
  • fd = 8,可读;
  • fd = 15,可写;

需要注意的是,它只报告"谁就绪了、发生了什么类型的事件"。不会帮你 read,也不会帮你 write,更不会处理业务逻辑。那是具体的事件处理函数要做的事情。

如果没有它,一个线程要同时等待多个连接几乎无法实现;有了它,一个线程就可以同时等待成千上万个 fd,只在真正有事件发生时才被唤醒。这正是 I/O 多路复用的价值。

下面这张图展示了一个线程如何同时等待多个 Handle。

一个线程等待大量连接

事件多路分离器解决的是"如何用一个线程等待多个事件源"。

事件就绪了,接下来该调用谁的什么方法?

Demultiplexer 或者就叫 epoll_wait 吧,好记一点。

它返回的是一个就绪的 fd,但不知道要对这个 fd 做读数据操作、还是建立连接操作、还是要处理一个超时任务。这些差异很大的业务逻辑,不能让 Demultiplexer 去管,否则它就不再是一个通用机制了。

本着职责分离的原则,不同模块处理不同事情,需要解耦。所以需要一层接口,规定"某种事件发生以后,应该调用什么方法",这就是 Event Handler

这也是在 ACE 中定义的一个名词,它是一个抽象的接口类,包含了以下的方法:

方法含义
get_handle()返回当前 Handler 关联的 Handle
handle_input()处理读事件
handle_output()处理写事件
handle_exception()处理异常事件
handle_timeout()处理定时事件
handle_close()处理关闭事件

这些方法不是随便设计的,而是对应常见事件类型:

注意 handle 和 handler 的区别,虽然差一个字符,但含义是千差万别的。

  • handle: 是事件源类型,也就是 fd
  • handler: 是事件源 fd 对应的处理函数

有了这层接口,Reactor 不需要知道一个连接内部怎么读数据、怎么解析协议、怎么生成响应,只需要知道:

  • 如果这个 Handler 的 Handle 可读,就调用 handle_input()
  • 如果这个 Handler 的 Handle 可写,就调用 handle_output()
  • 如果这个 Handler 超时,就调用 handle_timeout()
  • 如果这个 Handler 关闭,就调用 handle_close()

Event Handler 类似 C++ 中的抽象基类,只负责接口定义,不负责实现

这就是接口的价值:Reactor 只依赖这一层抽象,不依赖任何具体业务。不关心连接内部怎么读、怎么解析

这样它才能同时既处理监听 socket 的"客户端建连事件"、同时处理已连接 socket 的"读数据事件"、已连接 socket 的"可写事件"、定时器超时事件,这些完全不同的场景。

事件接口

我们可以看出,Event Handler 是 Reactor 和具体业务之间的桥梁。 Reactor 只认识 Event Handler。当事件发生时,只需要根据 fd 找到对应的 Event Handler,然后调用它就行了。

监听 socket 和连接 socket 的可读,能用同一套逻辑处理吗?

Event Handler 类似 C++ 中的抽象基类,只负责接口定义,接口本身不能干活。

监听 fd 可读和连接 fd 可读,虽然都叫"可读",但对应的功能是完全不一样的。监听 fd 事件就绪了,需要调用 accept 处理客户端连接;而连接 fd 就绪了,可以读取客户端发来的数据,或者可写了,则给客户端继续发送响应数据。

如果把这些差异很大的逻辑都塞进 Reactor 主循环,事件循环很快会变成一个大杂烩:

耦合的事件循环

这种写法一开始直观,代码全部放到一起,可读性是会好一点。但后续连接类型、事件类型和业务逻辑一多,那各个逻辑必然耦合在了一起,主循环就会越来越重,改一个地方容易影响另一个地方。此时就需要解耦了。

真正的解法是让每种事件都有自己独立的实现类,也就是 Concrete Event Handler,它是 Event Handler 接口的具体实现。也就是真正的事件处理函数。

常见的 Concrete Event Handler 包括:

  • Acceptor:负责处理客户端连接建立;
  • Connection Handler:负责处理已经建立连接上的读写事件;
  • Timer Handler:负责处理定时相关事件;
  • Signal Handler:负责处理信号相关事件。

它们都是 Event Handler 的不同实现。

Event Handler 类似 C++ 的基类,而 Concrete Event Handler 是派生类

事件多态继承

Concrete Event Handler 是真正的事件处理函数,也是真正会进行网络 I/O 系统调用的地方。例如:

  • 客户端建立连接,调用 accept;
  • 连接 fd 有事件可读,调用 read;
  • 连接 fd 有事件可写,调用 write。

而之前说的 epoll_wait 返回,只是返回已经就绪的 fd,是不会触碰任何的 I/O 系统调用。I/O 系统调用只在 Concrete Event Handler 中处理。之所以这么搞,还是为了职责分离,为了解耦。

加上这一层之后,结构就从大杂烩变成了各司其职:

事件解耦

侯捷老师常说,任何问题都可以通过增加一个间接层来解决。

Concrete Event Handler 就是 Reactor 模型里的这一层间接层:它把"事件调度"和"事件处理"隔开,让 Reactor 只需要知道 Event Handler 的存在,而无需认识具体的 Concrete,让具体事件处理可以自由扩展。这也是 C++ 中多态的体现。

以最常见的 Acceptor 和 Connection Handler 为例,看看它们各自负责什么。

Acceptor Handler 通常负责:

  • 监听套接字 listenfd
  • listenfd 可读时,调用 accept,处理客户端连接;
  • 创建新的连接对象;
  • 把新连接注册到 Reactor。

这里要特别区分 accept 和 Acceptor。两个是完全不同的概念。accept 是系统调用,用来处理客户端的连接,返回一个已经连接后的 fd;而 Acceptor handler 是负责调用它的具体事件函数。

概念含义
accept系统调用
Acceptor负责调用 accept 的具体事件处理器

Connection Handler 通常负责:

  • 监听已连接 fd 的读/写事件;
  • 当连接 fd 可读时,在事件处理函数中读取客户端数据;
  • 当连接 fd 可写时,在事件处理函数中发送响应数据给客户端;
  • 当连接关闭时,释放资源。

Timer Handler 通常负责处理超时任务、关闭空闲连接、触发周期性任务。

除了以上这些事件外,也支持扩展更多的事件。例如,管道事件、信号事件等都可以集成到 Reactor 中来,仅仅需要继承 Event Handler 实现管道 handler、信号 handler。

这就是 C++ 多态的威力,别小看了这个模型,扩展性非常强。

这也是高并发网络框架一个非常重要的功能,叫做统一事件源。

下面这张图展示了不同事件源如何对应到不同的 Handler。

统一事件源

这张图的关键点是:Concrete Event Handler 让不同事件拥有各自独立的处理逻辑。

这么多角色,谁来负责注册、等待和分发?

到目前为止,已经介绍了 Reactor 的 4 个角色:Handle 标识事件源,Demultiplexer 等待事件,Event Handler 事件处理函数抽象接口,Concrete Event Handler 真正的事件处理函数实现类。但它们还只是食材,谁来把这些食材组装起来、做出一道美味佳肴来呢?

答案是 Initiation Dispatcher 要解决的问题。

很多文章里会直接把它称为 Reactor,也有的地方叫做事件循环 loop。其实不对,Initiation Dispatcher 仅仅是 Reactor 中的一部分。

但单词太长了,不好记忆,我们还是遵从大家通用的叫法,就叫做事件循环 loop 吧。它是整个 Reactor 网络框架中的调度系统,它负责三类动作:

  • 注册、删除事件;
  • 进入事件循环并等待事件就绪;
  • 分发就绪事件。

注册事件

这里需要特别注意"注册"的方向:应用程序通常不是直接把 Handle(也就是 fd) 注册给底层的 epoll,而是先把 Event Handler 注册到 Dispatcher(事件分发器);

然后 Dispatcher 通过 handler.get_handle() 接口取得这个 Handler 关联的 Handle(fd),再把这个 fd 以及它关心的事件类型注册到 Demultiplexer(也就是 epoll)。

注册事件

所以更严谨的说法是:Handler 注册到 Dispatcher,此时 Dispatcher 内需要保存 Handle 和 Handler 的映射,也就是 fd 和事件处理函数的映射。

Handle 则将 fd 感兴趣的网络 I/O 事件,例如可读/可写注册到 Demultiplexer(也就是 epoll);

epoll 在将 fd 感兴趣的网络 I/O 事件注册到内核。

之所以这样设计,是为了让业务代码只需要面对 Handler 这个抽象对象,而无需直接操作底层 I/O 多路复用接口。

通常 Reactor 网络模型都是作为底层 lib 库角色,提供给业务进程加载。进程只需要调用 lib 库中的接口。一个库的好坏,其中一个判断标准,调用者是否需要过多的关注库中细节。

为什么这份映射必须由 Dispatcher 统一管理,而不是分散在各个 Handler 里自己去和操作系统打交道?

不同的 Handler 属于不同的模块,是分散到不同地方的。

如果没有这样一个中枢,每个 Handler 都要自己处理注册逻辑、自己等待事件、自己分发事件,Handler 之间难以协作,系统整体也难以维护。

Dispatcher 的价值,就是统一事件注册、统一事件等待、统一事件分发。

进入事件循环并等待事件就绪

事件循环 loop,平时大部分时间都阻塞在 epoll_wait 等 I/O 多路复用上。

一旦有就绪事件发生,线程就会被唤醒,epoll_wait 返回后,就可以获取到所有已经就绪的事件。

此时事件循环 loop 开始工作,根据 fd 找到对应的事件处理函数进行分发处理。

分发就绪事件

根据就绪的 Handle(fd) 和事件类型,找到对应的 Handler,并调用 Handler 中的事件处理函数。

例如:

  • listenfd 可读 → 找到 Acceptor → 调用 handle_input 处理客户端的连接事件;
  • connfd 可读 → 找到 Connection Handler → 调用 handle_input,读取来自客户端的数据
  • connfd 可写 → 找到 Connection Handler → 调用 handle_output,把数据发给客户端

下面我们看下 Dispatcher 如何把事件就绪和事件处理连接起来。

分发事件

Dispatcher 是 Reactor 模型的中枢,它把事件源、事件等待和事件处理连接在一起。

小结:5 个参与者分别解决了什么问题

到目前为止,Reactor 的 5 个参与者都介绍完成了。个别参与者的名字太长了,影响阅读体验,也确实不好记忆。没关系,你只需要知道有这么一个参与者,具体负责什么功能就行了,名字叫不出来,不要紧。

回头看,它们其实分别在回答一个具体问题:

参与者解决的问题
Handle哪个资源发生了事件?
Synchronous Event Demultiplexer如何同时等待多个资源的事件?
Event Handler事件来了以后应该调用什么接口?
Concrete Event Handler具体事件应该怎么处理?
Initiation Dispatcher如何注册、等待并分发事件?

它们不是孤立的 5 个术语,而是 Reactor 网络框架的必要组成部分。

沿着"事件源识别 → 集中等待事件 → 定义事件处理抽象接口 → 实现对具体事件的处理 → 事件统一注册与分发"这条链路,一步步形成 Reactor 网络框架的完整体系。

理解了这条链路,当你再回头看这 5 个名字的时候,就不会再觉得抽象了。

Reactor 的 5 个参与者,说白了就是 5 个模块。如果是我们自己来写一个网络程序,通常会将 5 个模块所实现的功能耦合在一起。而 5 个参与者对应 5 个模块,每个模块职责分离,只实现一个功能,符合单一原则。

单线程5个参与者

这 5 个参与者同处一个 Reactor 线程(以 Dispatcher 为核心的事件循环进行统一调度管理)

需要注意的是,Reactor 的 5 个参与者,或者说 5 个模块,都处于同一个线程,也就是 Reactor 线程。这 5 个参与者不是 5 个线程,而是同一个 Reactor 线程内协作的 5 个模块,各司其职、职责分离

即使对于主从 Reactor 模式,虽然有多个 Reactor 线程,但每个线程各由这 5 个参与者组成。

主从reactor,内部各5个参与者

因此即使扩展成主从 Reactor,多线程也只是「多套」这样的 5 个参与者;单个线程内部的结构与循环始终不变。

三、把 5 个参与者整合起来看 Reactor

前面我们把 5 个参与者拆开讲了,现在要把它们合起来,这一步很重要。

概念是散的:很多人理解不了 Reactor,一方面是因为某个概念特别难,另一方面是因为这些概念是孤零零的一个点,没法完整将这些孤零零的点,连点成线串起来,形成一个体系。

  • Handle 是 fd;
  • Handler 是回调;
  • epoll 是多路复用;
  • Dispatcher 负责循环。

这些说法都没错,但如果不将这些碎片化的概念,合成一个完整事件流,就很难真正理解 Reactor。因此我们来看下,一个完整的事件处理流程是怎样的。

一个完整的事件处理流程是什么样的?

先看总图。

事件处理总流程

这张图要抓住一条主线:

  1. Handler 注册到 Dispatcher;
  2. Dispatcher 取得 Handle(也就是 fd) 并保存 fd 和 Handler 的映射;
  3. Dispatcher 把 Handle 注册给 Demultiplexer(也就是 epoll);
  4. Dispatcher 调用 Demultiplexer 等待事件;
  5. Demultiplexer 返回就绪事件列表;
  6. Dispatcher 根据 Handle 找到 Handler 并分发事件。

这就是 Reactor 的完整事件的处理流程,接下来我们一步步进行拆解,回答几个大家可能存在的疑问。

Handler 为什么要先注册?

先注册,后等待:Reactor 不是临时看到事件再去找处理函数,它必须提前知道:

  • 我应该监听哪些 Handle?
  • 每个 Handle 关心什么事件?
  • 事件发生后应该交给哪个 Handler?

因此,具体的 Concrete Handler 必须要先注册到 Dispatcher 中才行,先建立好映射关系。

例如 Acceptor 这个 Handler 将可读事件处理函数注册到 Reactor:

我关心监听套接字的可读事件。如果监听套接字可读,说明有客户端建连请求,此时会调用监听套接字对应的读事件处理函数。

建立连接后的 Connection Handler 将可读事件处理函数注册到 Reactor:对于可写事件也是一样的,都需要提前注册

我关心已连接套接字的可读事件。如果已连接套接字可读,此时会调用已连接套接字对应的读事件处理函数。

这一步主要的目的是为了确定"事件发生前的绑定关系"。如果没有注册,那事件发生时,Reactor 就无法知道事件要分发给谁进行处理了。

为什么 Dispatcher 要保存 Handle 到 Handler 的映射?

从系统事件到业务对象:事件多路分离器(epoll)返回的通常只是 Handle 和事件类型,例如 fd = 8,可读,但它不会告诉你这是哪个连接对象、哪个业务对象、哪个 Handler。所以 Dispatcher 必须维护一张映射表,例如:

HandleHandler
Handle 3Acceptor
Handle 8Connection Handler A
Handle 12Connection Handler B
Handle 20Timer Handler

fd = 8 可读时,Dispatcher 才能找到:fd = 8 对应 Connection Handler A,事件类型是可读,所以调用 Connection Handler A 的事件处理函数。这一步解决的是"从系统事件到业务对象"的转换。

为什么事件多路分离器(epoll)只负责等待?

因为等待和处理是两种不同的职责:事件多路分离器主要做这件事:

看看这些 fd 里面,哪些已经可读、可写或异常。

但它不知道业务的含义。比如同样是"可读":

  • listenfd 可读:表示有新连接,需要进行 accept 处理客户端连接;
  • connfd 可读:表示客户端发来数据,需要 read 读取数据;
  • timerfd 可读:表示定时器到期,需要调用定时器的处理函数。

如果也让事件多路分离器理解这些业务含义,那和把各种事件的处理逻辑写在事件循环里就没区别了。因此 Reactor 模型刻意把职责分开:

  • Demultiplexer:只发现就绪事件;
  • Dispatcher:只负责分发事件;
  • Handler:只处理具体的事件。

这就是 Reactor 设计比较好的一面。它不是简单地使用 epoll 这个 I/O 多路复用机制,而是把系统层通知的事件和应用层处理逻辑隔离了。

为什么 Handler 处理完还要回到事件循环?

事件是持续发生的:因为网络服务器不是处理一个事件就结束,连接上的事件是持续发生的:

  1. 客户端建立连接;
  2. 客户端发送请求;
  3. 服务端读取请求;
  4. 服务端生成响应;
  5. 服务端发送响应;
  6. 客户端继续发送下一个请求;
  7. 连接空闲;
  8. 连接关闭。

所以 Handler 处理完当前事件后,必须要把控制权交回到 Reactor 的事件循环。Reactor 继续等待下一批事件。整个服务器就像一个不断旋转的永动机:

等待 → 分发 → 处理 → 等待 → 分发 → 处理……

这也是它被称为 Reactor 反应堆的原因之一。有事件发生,它就做出反应;没有事件到来,它就睡眠等待。

这套思想是通用的,很多开源项目都在使用。

例如 Linux 内核协议中,协议栈本身负责转发,然后调度 netfilter 的钩子函数处理业务逻辑,例如 NAT, ACL 访问控制等等。钩子函数返回一个处理结果,协议栈根据这个结果码继续转发或者继续调用其他钩子函数。

大道至简,思想是通用的。

为什么 Reactor 线程不能做耗时任务?

事件循环不能被阻塞:因为 Reactor 线程通常负责整个事件循环。如果某个 Handler 在事件循环里执行了耗时任务,比如:

  • 复杂计算;
  • 慢 SQL;
  • 远程 RPC;
  • 磁盘大文件处理;
  • 长时间加锁。

那么 Reactor 线程就不能及时回到事件等待和事件分发,结果是:

一个连接的慢处理,拖慢所有连接的事件响应。

这也是为什么高并发服务器要设置为非阻塞的原因。一旦在事件处理函数中执行了阻塞操作,你线上运营过程中,会发现 QPS 请求数将直线下降。

因为,不适合在事件循环里直接执行逻辑非常耗时的操作;耗时业务通常应该交给业务线程池,处理完成后再把结果交回 Reactor 线程进行回包。

引入业务线程处理计算逻辑,和 Reactor 线程解耦。这是 Reactor 网络框架演进出来的新方案,我们下一篇文章再详细展开。本文先聚焦 Reactor 内部的处理流程。

新连接到来的完整流程

下面专门看 accept 在 Reactor 中的位置。新连接到来时流程大概是这样:

accept完整流程

别把 accept 当参与者,它只是系统调用:这里最容易混淆的是,accept 不是 Reactor 模型的参与者,它只是一个系统调用,Acceptor 才是一个 Concrete Event Handler。它的职责是:

  • 监听客户端的连接
  • 当监听套接字变为可读时调用 accept 函数;
  • 拿到连接后的 fd,也就是 connfd
  • connfd 创建读事件处理函数;
  • 把新连接 fd 注册到 epoll。

所以准确地说:新连接事件由 epoll 发现,由 Dispatcher 分发,accept 动作由 Acceptor 执行,新连接 connfd 再重新进入 Reactor 管理。这条链路非常关键,因为它说明 Reactor 不只负责已有连接的读写,也负责把新连接纳入事件循环。

已连接 socket 的读写流程

读事件处理:

连接建立后,Reactor 要继续管理这个连接上的读数据事件,接收客户端发来的数据并解析。读事件流程如下:

读事件流程

事件循环 loop 平时就阻塞在 epoll_wait 系统调用上,等待客户端发来数据。一旦内核收到客户端发来的数据后,就会唤醒 Reactor 线程,epoll_wait 就会返回已经就绪的事件。

事件循环拿到已经就绪的事件后,根据 fd 查找到对应的事件处理函数(位于 Connection Handler),分发给该事件处理函数

事件处理函数内,调用 read 系统调用,真正读取客户端发来的数据。

epoll 只通知「可读」,真正的 read、协议解析、响应生成,都由 Connection Handler 完成。

写事件处理

写事件流程

写事件流程稍微特殊一点:通常不是一直监听写事件,而是"有数据要写但一次没写完"时,才关注写事件。默认不监听写事件;只有一次没写完时才注册写事件,并且写完立即取消。

如果一次性写完了数据,也是不需要注册写事件的。

那一次性没有写完,Connection 这个 Handler 内部就要维护一个发送缓冲区,保存未发送完成的数据,等下一次可写事件触发时继续发送。

之所以这么操作,因为套接字默认就是可写的,如果在还不需要立即给客户端发送数据时就注册了写事件,那 epoll_wait 就会频繁触发,无形中增加系统调用的次数。从而也导致事件循环被无意义唤醒。

只通知,不代劳:这里有一个非常重要的点。不管是读事件还是写事件。Reactor 都不自动帮你读写,它只是告诉你"现在可以读了、或者现在可以写了、这个连接异常了、这个连接关闭了"。

真正的 readwrite、协议解析、响应生成,仍然由对应的 Handler 完成。

四、回答 Reactor 网络模型几个有疑问的地方

一提到 Reactor,就认为是 epoll

我想一提到 Reactor,很多人就会想到 epoll。epoll 仅仅是 Reactor 5 个核心参与者中的一个。Reactor 的核心不是 epollepoll 只是 I/O 多路复用的一种具体实现方式。在不同系统上,Reactor 可以具有不同的实现方式,例如:

  • select
  • poll
  • epoll

Reactor 是一种高并发服务器的网络设计模式,将 5 个参与者整合起来的设计模式。而 epoll 是操作系统提供的能力,不要把模式和具体系统调用混在一起。

不用 Reactor, 照样可以写出网络服务器

我们初次写一个网络服务器时,必然会将各个角色揉成一个大杂烩,不同事件的处理都在一个事件循环中处理,耦合非常严重,也不方便扩展。而 Reactor 模式真正重要的是职责分离,它把一个复杂网络服务器拆成了几个清晰角色,进行解耦:

角色职责
Handle标识事件源
Synchronous Event Demultiplexer等待事件
Event Handler定义处理接口
Concrete Event Handler实现具体处理逻辑
Initiation Dispatcher注册、等待、分发事件

每个角色只解决自己的问题,这让网络服务器可以在高并发场景下保持清晰结构:这才是 Reactor 模型真正值得学习的地方。

  • 操作系统负责发现 I/O 就绪,
  • Reactor 负责事件调度,
  • Handler 负责事件处理,
  • 业务线程负责耗时任务。

如果自己想实现一个 Reactor 框架,要如何处理呢?

想必很多人对 Reactor 只有一个浅显认识,如果要自己实现一个 Reactor 网络模型,建议不要一开始就追求全面,想一次性支持多线程、连接池、定时器、异步任务队列等等。最好的处理方式是:先实现一个最小可用的单线程 Reactor,然后逐步添砖加瓦。

大体可以按照下面的思路来执行。

1. 抽象出 Handle,标识出具体的事件源。

这是第一步,需要先想清楚我们需要处理哪些事件源。

最基础的网络服务器至少有两类 Handle。一个是监听套接字、另一个是已经连接的套接字 fd。

后面可以继续扩展出定时器 fd、信号 fd 等等。

这一步的目标是建立统一视角:

不管它是监听 socket、连接 socket,还是定时器,本质上都是一个可以产生事件的 Handle。专业术语叫做"统一事件源"

2. 设计 Event Handler 接口,定义事件处理函数

有了 Handle 以后,接下来定义事件处理接口。最基本的 Handler 至少需要处理读事件、处理写事件、处理关闭事件、处理错误事件等等。

Reactor 只应该知道这个 Handler 关心哪个 Handle、事件来了以后应该调用哪个入口。这样设计之后,后续新增 Handler 类型时,Reactor 不需要大改。

3. 实现具体 Handler,不同事件对应不同处理函数

最基础的两个是 Acceptor 和 Connection Handler。分别处理客户端建立连接、以及建立后的数据读写

4. 封装事件多路分离器,例如 epoll

这一步,主要是对 I/O 多路复用的封装。例如:

  • 注册/修改/删除 Handle(也就是 fd)关注的读/写事件;
  • 等待读写事件的发生;
  • 返回就绪事件列表。

它不需要知道 Handler 是什么,也不需要知道事件对应的处理函数是什么。它只需要等待就绪事件发送、返回已经就绪的事件.

这一步的目标是:让一个线程可以等待多个 Handle 上的 I/O 事件。

5. 实现 Dispatcher,也就是事件循环

Dispatcher 这个事件循环,也是 Reactor 的调度系统。这一步的目标是把"已经就绪的事件"和"事件处理"连接起来。

因此需要维护 Handle → Event Handler 的映射。

它对外提供注册 Handler、移除 Handler、修改 Handler 关注的事件、启动事件循环、停止事件循环这几个接口。

而事件循环内部做的事情是:

  • 调用事件多路分离器等待事件;
  • 拿到就绪事件列表;
  • 逐个找到对应 Handler;
  • 根据事件类型调用对应方法;
  • 处理完后继续睡眠等待。

6. 实现 accept Handler,处理客户端的连接

7. 实现 Connection Handler, 处理客户端数据的 read/write 操作

五、写在最后

Reactor 是一种高并发服务器网络框架。它解决的核心问题是:如何用少量线程管理大量连接上的 I/O 事件,让线程阻塞在 epoll_wait 等系统调用上,而不是阻塞在 read/write/accept 等系统调用。

Reactor 模式包含 5 个核心参与者:

角色职责
Handle标识事件源
Synchronous Event Demultiplexer等待事件
Event Handler定义处理接口
Concrete Event Handler实现具体处理逻辑
Initiation Dispatcher注册、等待、分发事件

它们之间的关系可以用一句话来概括:

Handle 标识事件源,Demultiplexer 等待事件,Dispatcher 分发事件,Handler 处理事件。

理解 Reactor 时,有几个地方是比较容易让人觉得有歧义的。

  • accept 是系统调用,不是 Reactor 核心参与者;
  • Acceptor 是 Concrete Event Handler,用来处理新连接;
  • Dispatcher 是事件分发,事件循环;
  • Handler 是事件处理逻辑,不是线程,也不是连接本身;
  • Main Reactor 和 Sub Reactor 是工程扩展,不是经典 Reactor 的 5 个参与者。

如果要想真正掌握 Reactor,我们需要在脑子里清楚地跑出这条链路:

事件源注册 → 操作系统等待 → 事件就绪 → Reactor 分发 → Handler 处理 → 回到事件循环

当把这条链路跑通了,我们对 Reactor 网络模型的理解就不会那么的畏惧,后续再看 Reactor 框架的演进过程,也能做到知其所以然。

读者肯定会觉得文章还缺少点什么,怎么没有讲 Reactor 模型的演进过程。例如单 Reactor,多线程 Reactor, Main/Sub Reactor 等。别急,我们留到下一章再详细展开

也许读者也好奇,我是怎么知道 Reactor 的 5 个参与者的?

这不是我造出的概念,前辈早替我们做了总结,最早可以追溯到 ACE 技术内幕这本书,就介绍了这 Reactor 5 个参与者概念。只是比较抽象,没那么好理解。

在腾讯阿里这些年,踩了无数的坑,积累了不少高并发服务器经验。和你们一样,当年我对 Reactor 也是一知半解,因此将 Reactor 这个抽象的概念,尽我能力给讲清楚,希望对各位读者有所帮助。

读完文章,你能手写一个 Reactor 网络框架吗?

如果不行,说明我没有把 Reactor 讲清楚,功力有限。

业界很多开源的网络框架都有实现 Reactor,其中陈硕的 muduo 是我阅读过的几个开源项目中,把 Reactor 发挥得比较好的一个网络框架。只有几千行代码,比较轻量,一个月时间就能看懂,感兴趣可以去阅读。

Reactor 讲完了,我尽可能站在你们的角度思考问题。然而每个人理解是不一样的,难免会有疑问的地方。有任何疑问,我们评论区里交流。


图解高并发,小白也能看懂。每周更新一篇高并发干货。觉得有用,点下方关注我 👇

关注 apelife

最后更新于:

基于 VitePress 构建