前言:
- Doug Lea可伸缩IO原文:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
- Reactor在很多地方会用到,例如Java提供的NIO、Redis、Thrift还有Netty整体就是基于Reactor的框架。
- 下面是递进的对IO模型的概述,最后一种就是最常用的Reactor形态了。
1. Classic Service Designs
- Classic Service Designs:基础写法的Socket服务端对每个客户端都会创建一个线程,这样资源充足的情况下吞吐量高,一旦连接数过多会导致服务端资源耗尽,效率就会受影响。
- 通讯的基本流程结构分为以下5个步骤,中间3个部分通常需要自定义实现:
- Read request
- Decode request
- Process service
- Encode reply
- Send reply
//常见Socket服务端,可以看到循环创建线程,接收socket
main(){
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
new Thread(new Handler(ss.accept())).start();
}
//handle()
handle(socket){
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output)
}
Classic Service Designs
2. Event-driven Designs& Reactor单线程设计
-
Event 事件
:在通讯中就是套接字触发某些特定状态例如- 服务器接到客户连接时会触发
SelectionKey.OP_ACCEPT
- 连接就绪时会触发
SelectionKey.OP_CONNECT
- 客户端向服务端发送的数据可读时会触发
SelectionKey.OP_READ
- 服务端可写回数据时会触发
SelectionKey.OP_WRITE
- 服务器接到客户连接时会触发
-
handler 事件处理器
:一个个函数,事件和handler
最初时就会一对一绑定,监听的每个套接字触发Event
时都会调用对应的handler
-
Event-driven Designs
:上面两个概念了解后就大致知道了事件驱动的流程,但谁负责提供监听n多个套接字的事件,这个主要依赖内核机制如pool/epoll/select/kqueue,用户程序端不断去访问,没有事件触发时返回集合就是空,如果集合有数据,线程就根据Event
执行对应的handler
,在程序端来看一个线程只要不断轮询就达到负责多个连接的目的了。 - 总结说就是内核产生的套接字
Event
回调自定义的handler
,就是所谓的事件驱动,这时整体调用结构就发生了根本性的变化。
单线程reactor - 其中Reactor模块负责检测新的事件集合,并根据事件指定到某个
handler
处理。 - 整体单线程设计,但这种方式虽然是
Reactor
而且也可以实现NIO不过也不常用,因为整体还是单线程处理,比如某个handler
处理时间过长,或触发事件集合过多,会导致后续的handler
处理延迟,这时候自然就想到了多线程。
3. Worker Thread Pools
-
进化版:之前读写、编解码、业务都在一个线程,这时把handler编解码 + 业务逻辑放入线程池中就变成了这个样子,业务处理虽然被解放,但是量继续增大通信(read/send/accept)部分也有可能成为瓶颈,那就继续扩充。
Worker Thread Pools.png
4. Worker Thread Pools
- 最终版本:Reactor分成两个部分,
mainReactor
负责接收连接,握手完成后发送到subReactor
,subReactor
负责和客户端的通信、handler调用,具体逻辑服务部分也已经有对应的线程池执行,目前就是最优选择了。
Using Multiple Reactors.png - 总结:
- NIO主要是优化多客户端调度,让更少的服务端线程承担更多的连接。模型进化的过程只是让资源分配的粒度更细,更加解耦且容易控制。
- 当然如果不知道怎么选择,其实大部分情况下NIO这种都是最优选择(AIO暂时不常用)。
网友评论