目录
1. 简介
Reactor模式(反应器模式)是一种处理一个或多个客户端并发交付服务请求的事件设计模式。
Reactor 模式被广泛应用在设计高性能IO方面上,大多数IO相关组件如 Netty、Redis 就都在使用的该设计模式。
2. 为什么要使用该模式
在以前传统的Java IO网络编程方式就是在服务器调用一个 while 循环,然后在其中调用 serverSocket 的 accept 阻塞方法来不断监听端口是否有新的 Scoket 连接,这是一种 BIO 的模式。
Server端代码如下:
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by vMars on 2019/9/15.
*/
public class Server {
public static void main(String[] args) throws IOException {
//监听 8000 端口
ServerSocket serverSocket = new ServerSocket(8000);
// 新建一个线程,防止 accept() 阻塞主线程
new Thread(() -> {
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
//获得输入流
InputStream inputStream = socket.getInputStream();
int len;
byte[] data = new byte[1024];
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
String message = new String(data, 0, len);
System.out.println("获得客户端传来消息:" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
} catch (IOException e) {
System.out.println("服务端启动失败");
e.printStackTrace();
}
}
}).start();
}
}
在这种IO方式中通过 while循环来监听端口是否有新的连接,建立了连接之后我们会新建一个线程来处理IO,所以这个时候我们是一个 Scoket连接对应一个线程。
我们可以可以把它类比成餐厅(服务器)中顾客(客户端的Scoket连接)与服务员(线程)的关系:
(图来自 https://lotabout.me/2018/reactor-pattern/,很生动的解释了 为什么使用Reactor)
但是线程是很宝贵的资源,如果同时有大量的 Scoket 连接就会创建大量的线程,且线程的销毁和创建也是需要代价的,这个时候可能会想到可以用线程池解决。
但线程池虽然能让线程重复利用,避免了重复创建销毁线程,但是这并没有解决线程粒度太大的问题,每个线程都需要各自完成交互的全过程(如读取内容,解码,逻辑处理,编码,内容回复等),所以线程池并不是最好的解决方案。
所以我们可以考虑降低线程的粒度,我们将过程的操作细分为多个子过程 handler,每一个 handler 处理一种 event,然后通过一个 selector 来管理,连接 channel 进来后为其注册感兴趣的事件 event,当 channel 触发某个事件后 selector 可以监听到然后再进一步处理。
这样我们就可以通过事件驱动降低线程的粒度了,这就是 Reactor模式。
3. Reactor 的结构
(图来自 https://www.cnblogs.com/winner-0715/p/8733787.html)
具体详细的分析可以看上面的链接,这里记录我理解的大致流程:
1. 首先我们会在 Initiation Dispatcher(即是 Reactor 模式的核心处理器) 注册我们的 Concrete Event Handler(即是我们自己编写的具体处理器),在注册的时候我们会指定感兴趣的事件 Event,如读事件READ、新连接事件ACCEPT等事件。
2. 然后会使用 Synchronous Event Demultiplexer 同步阻塞的等待一个或多个事件的发生,这里的 Synchronous Event Demultiplexer 就是使用了IO多路复用的机制,如 Java NIO中提供的 selector 。
3. 当我们所监听的 Handler 变为 ready 时,Synchronous Event Demultiplexer 会通知 Initiation Dispatcher,然后 Initiation Dispatcher 会根据这个 Handler 去分发恰当的事件处理器 Concrete Event Handler。
4. Reactor 模式的实现
4.1. 单 Reactor 线程模式
在该种模式下我们使用的是一个单线程的 Reactor,acceptor() 处理器注册了 ACCEPT 事件,即连接事件,当有连接请求时 Reactor 会将其分发给 acceptor() 处理。
但在这种模式下,Reactor 线程不但要处理 accept()、read()、send(),连非IO业务也要处理,如果业务逻辑复杂,这可能会使 Reactor 线程无法处理其它事件的响应。
为了避免这种事情发生,我们需要把非IO业务逻辑处理交给子线程。
4.2. 单 Reactor 线程模式 + 线程池
通过加入工作线程池,把具体的逻辑操作交由子线程,提高了 Reactor 线程的IO响应时间,但是这样的模式还是存在着缺陷。
就是Reactor 线程要处理包括I/O的accept()、read()、write()以及connect()操作,当同时有大量的连接建立时,单线程的 Reactor性能会下降,然后可能会使大量客户端连接超时,最终使大量消息积压和连接超时。
为了解决这个问题我们采用多 Reactor 模式。
4.3 多 Reactor 模式
在多Reactor 模式中,我们分为 mainReactor 和 subReactor,每一个 Reactor 都会有自己的 Selector 与不同的事件循环逻辑。
其中 mainReactor 主要负责接受客户的连接请求,然后将建立的 ScoketChannel 传递给 subReactor,由 subReactor 来完成和客户端的通信。
而 subReactor 一般会有多个,这可以很好的解决单 Reactor 线程模式下的瓶颈。