单线程 Reactor 模式
目录
Reactor 模式是什么?
为什么要学 Reactor 模式?
多线程OIO 的致命缺陷
单线程 Reactor 模式
如何用 Java NIO 实现单线程 Reactor?
单线程 Reactor 模式 Echo 服务器的实现过程
Reactor 模式是什么?
Reactor 模式是一种经典的高性能网络通信模型,它采用事件驱动的方式来处理大量的客户端连接请求和数据读写操作。
在 Reactor 模式中,系统通过一个或多个 Reactor 线程负责监听并响应 I/O 事件(如连接建立、读写就绪等),当某个 I/O 事件发生后,Reactor 会将该事件分发给对应的 Handler(处理器) 进行具体的业务处理。这种模式的核心思想是将 I/O 的事件响应与业务处理解耦,从而实现高效、非阻塞的网络通信机制。
Reactor 模式中主要包含两个核心角色:
Reactor 线程
Reactor 线程负责监听所有的 I/O 事件,并在事件就绪时及时将事件分发给对应的 Handler。这相当于事件驱动模型中的“事件分发器”。
Handler 处理器
Handler 是具体的事件处理者,它绑定在某个具体的 I/O 事件上。当事件发生并由 Reactor 分发后,Handler 负责执行业务逻辑的处理,例如建立连接、读取数据、处理请求、返回结果等操作。它的执行是非阻塞的,能够快速完成任务并返回控制权。
通过这种机制,Reactor 模式可以在有限的线程资源下处理海量并发连接,有效避免传统阻塞 I/O 模型中“一个连接占用一个线程”的资源浪费问题,非常适合构建高并发、高吞吐量的网络服务系统。
为什么要学 Reactor 模式?
在高性能网络编程中,Reactor 模式是一项必须掌握的核心技术。随着互联网的高速发展,网络通信需求日益增长,传统的阻塞式 I/O 编程方式已经无法满足高并发、高吞吐量的应用场景。而 Reactor 模式正是为了解决这些问题而设计的一种高效、可扩展的事件驱动模型,广泛应用于现代高性能服务端开发中。
目前市面上众多著名的服务器软件或中间件,例如高性能 Web 服务器 Nginx、高性能缓存数据库 Redis、以及高性能通信框架 Netty,其底层都是基于 Reactor 模式实现的。这些项目在实际生产中广泛应用,且其性能表现卓越,充分说明了 Reactor 模式在构建高并发系统中的重要性。
从开发者的角度来看,想要胜任高性能服务器的设计与开发工作,Reactor 模式是必须理解并掌握的。它不仅涉及网络编程的基本原理,更体现了系统设计中关于“事件驱动”、“非阻塞 I/O”、“资源复用”等关键思想。掌握了 Reactor 模式,才能深入理解像 Netty、Redis、Nginx 等系统背后的核心机制,也更容易阅读和理解这类中间件的源码。
多线程OIO 的致命缺陷
OIO(阻塞式 I/O,Blocking I/O)的核心特点是:所有 I/O 操作(如读取数据、写入数据)都会阻塞线程,直到操作完成。
原始做法(OIO):
while (true) {socket = accept(); // 阻塞等待连接handle(socket); // 处理数据(也阻塞)
}
问题: 如果 handle(socket) 没完成,后续连接无法接入,导致服务吞吐低。
改进:Connection Per Thread 模式
while (true) {Socket socket = serverSocket.accept();new Thread(new Handler(socket)).start();
}
while (true)和serverSocket.accept()的作用和之前一样:持续阻塞等待新连接。
改进:new Thread(new Handler(socket)).start()
当一个新连接到来(socket对象创建后),不再在当前线程处理,而是新建一个线程,将连接交给这个线程的Handler处理。
当前线程(监听线程)立刻回到while循环,继续执行accept(),等待下一个新连接。
优点:
解决阻塞监听问题。缺点:
线程数随着连接数增长,资源占用非常大;上限受限于操作系统线程调度能力;
线程切换、销毁、上下文开销高。
思考:一个线程处理多个连接可行吗?
答案:不太行。
传统 OIO 下:读/写都是阻塞的;
即使能同时绑定多个连接,一次只能处理一个 IO 操作;
多线程 IO 在高并发下仍会面临线程资源瓶颈。
解决方案:Reactor 模式
优点:
控制线程数量;单线程可处理大量连接;
基于事件驱动,效率高。
单线程 Reactor 模式
单线程 Reactor 是Reactor 模式最简单的版本,即 Reactor 和所有 Handler 都运行在同一个线程中。
单线程 Reactor 模式结构图
核心思想是通过一个线程来完成对所有 I/O 事件的监听与分发,同时由各类 Handler(处理器) 执行具体的业务逻辑处理。
整体流程:
1. 客户端Client 发起连接或数据传输;
2. Reactor线程 通过 Selector 检测到某个事件(如连接建立、数据可读)
3. Reactor线程 调用 分发器dispatch 将事件分发给对应的 Handler;
4. Handler 完成特定的操作(如接收连接、读取数据、处理业务、写入响应)
5. 所有操作均在同一个线程中串行完成,不涉及线程切换。
单线程 Reactor 的优点
实现简单,线程模型清晰;
没有多线程带来的并发控制开销(如锁、线程安全);
适合连接数不是特别多、业务处理较快的系统。缺点:
所有操作在一个线程中串行执行;
一旦某个 Handler 执行时间过长,会阻塞后续事件的处理;
不适用于高负载或复杂业务场景。
如何用 Java NIO 实现单线程 Reactor?
关键类:SelectionKey
主要用两个方法:
attach(Object o)
将任意对象(通常是 Handler)绑定到 SelectionKey。
例子:selectionKey.attach(handler);
attachment()
获取之前绑定的对象。
例子:Handler handler = (Handler)selectionKey.attachment();
使用场景:当 IO 事件发生后,selector 返回 selectionKey,通过 attachment() 拿到绑定的 Handler 实例,从而调用其处理逻辑。
在单线程 Reactor 中,核心是使用 attach() 和 attachment() 机制将 IO 事件与 Handler 解耦:
事件注册时:绑定 Handler 实例;
事件触发时:取出 Handler 实例并执行处理逻辑。
单线程 Reactor 模式 Echo 服务器的实现过程
Echo 服务器是一种最简单的网络服务器模型,核心功能是 接收客户端发送的数据后,原封不动地将数据回传给客户端
一、整体设计思路(单线程 Reactor 模型)
这个 EchoServer 的功能是:接收客户端连接、读取客户端发送的消息,然后原样返回(回显),即回显服务器。
为了实现该目标,基于 Reactor 模式构建,设计了三个关键类:
类名 | 作用 |
---|---|
EchoServerReactor | 反应器,负责监听事件并分发 |
AcceptorHandler | 连接处理器,处理新连接 |
EchoHandler | IO处理器,处理读写事件 |
二、EchoServerReactor 类(主类)
构造方法
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new AcceptorHandler());
注册 OP_ACCEPT:监听“新连接到来”事件。
attach Handler:将连接处理器绑定到该事件上(后续事件发生时可快速取出处理逻辑)
run() 主循环
while (!Thread.interrupted()) {selector.select(); // 阻塞等待事件dispatch(sk); // 分发事件
}
使用 Selector 轮询 I/O 事件;
一旦有事件就绪,调用 dispatch() 方法处理。
三、事件分发器 dispatch()
Runnable handler = (Runnable) sk.attachment();
if (handler != null) handler.run();
取出事件对应的 handler(之前通过 attach() 绑定的);
直接调用其 run() 方法完成业务处理。
四、AcceptorHandler 类(新连接处理器)
SocketChannel channel = serverSocket.accept();
new EchoHandler(selector, channel);
接收新的连接,返回一个 SocketChannel;
为新连接创建专属的 EchoHandler 实例来处理后续 I/O 事件;
EchoHandler 绑定读写事件,负责处理数据传输。
五、EchoHandler 类(数据读写处理器)
构造方法
sk = channel.register(selector, 0); // 先注册,获取 selectionKey
sk.attach(this); // 将当前处理器绑定为事件处理器
sk.interestOps(OP_READ); // 设置感兴趣事件:读
首先将 channel 注册到 selector,但初始不监听任何事件(interestOps=0)
然后将 EchoHandler 自己作为附件绑定到 selectionKey;
最后设置感兴趣的事件为“读”。
注意:注册和设置监听事件分开进行,以便先完成 attachment,再监听具体事件。
核心 run() 方法
if (state == SENDING) {// 发送数据到客户端channel.write(byteBuffer);byteBuffer.clear();sk.interestOps(OP_READ);state = RECEIVING;
} else if (state == RECEIVING) {// 接收客户端数据channel.read(byteBuffer);byteBuffer.flip();sk.interestOps(OP_WRITE);state = SENDING;
}
这是一个状态机模型:
RECEIVING:读取客户端发送的数据;
SENDING:将数据回显回去;
每次完成读或写后,切换感兴趣事件和状态,下一次事件触发时再进入另一种操作;
使用 ByteBuffer 实现读写缓存和状态切换。
六、整体流程
客户端连接
↓
Reactor (Selector监听)
↓
accept 事件触发
↓
AcceptorHandler.accept() 处理连接
↓
创建 EchoHandler (attach 到读事件)
↓
读事件触发 -> EchoHandler.run() 读取数据
↓
设置写事件
↓
写事件触发 -> EchoHandler.run() 写数据
↓
回到读事件,等待下一次读写
尚未完结