一、概述
在上一篇博客《Java 传统IO和NIO》 中,我讲述了java传统IO与NIO之间的区别以及NIO给我们带来的在网络编程上的性能的提升。然而,Java NIO以及后面要介绍的netty网络框架都是有一套理论在背后支撑的,那就是reactor模式的应用。
二、什么是reactor模式?
reactor模式翻译过来叫做反应器模式,通常我们都直接叫做reactor模式。
reactor模式是一种事件驱动模式,用于处理一个或者多个客户端发过来的请求,服务端会有一个处理器对到来的请求进行分离,并且将这些请求分发给对应的请求处理器进行处理。reactor的结构图大概如下图所示:
三、reactor模式的角色构成
从上面的结构图可以看出,Reactor模式由五中角色构成。分别是Handle(句柄或者描述符)、Synchronous Event Demultiplexer(同步事件分离器)、Event Handler(事件处理器)、Concrete Event Handler(具体事件处理器)、Initiation Dispatcher(初始分发器)。
- Handle(句柄或者描述符):本质上表示一种资源,是由操作系统提供的;该资源用于表示一个个事件,比如说文件描述符,或者针对于网络编程当中的Socket描述符。事件既可以来自于外部,也可以来自于内部;外部事件比如说客户端的链接请求,客户端发过来的数据等;内部事件比如说操作系统产生的定时器事件等。它本质上就是一个文件描述符。Handle是事件产生的发源地。
- Synchronous Event Demultiplexer(同步事件分离器):它本身是一个系统调用,用于等待事件的发生(事件有可能是一个,也有可能是多个)。调用方在调用它的时候会被阻塞,一直阻塞到同步时间分离器上有时间产生为止。对于linux来说,同步事件分离器直接就是常用的I/O多路复用机制,比如说select、poll、epoll等。在Java NIO领域中,同步事件分离器对应的组件就是Selector;对应的阻塞方法就是select()方法。
- Event Handler(事件处理器):本身由多个回调方法构成,这些回调方法构成了与应用相关的对于某个事件的反馈机制。
- Concrete Event Handler(具体事件处理器):是事件处理器的实现。它实现了事件处理器所提供的各个回调方法,从而实现了特定于业务的逻辑。它本质上就是我们编写的一个个的处理器的实现。
- Initiation Dispatcher(初始分发器):它本身定义了一些规范,这些规范用于控制事件的调度方式,同时又提供了应用进行事件处理器的注册、删除等操作。它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过同步事件分离器来等待事件的发生。一旦事件发生,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件。
四、reactor模式的流程
还是根据上面的图,可以看出reactor模式的流程:
-
当应用向Initiation Dispatcher注册具体的事件处理器时,应用会标示出该事件处理器希望Initiation Dispatcher在某个事件发生时向其通知的该事件,该事件与Handle关联。
-
Initiation Dispatcher会要求每个事件处理器向其传递内部的Handle。该Handle向操作系统标示了事件处理器。
-
当所有的事件处理器注册完毕后,应用会调用handle_events方法来启动Initiation Dispatcher的事件循环。这时,Initiation Dispatcher会将每个注册的事件处理器的Handle合并起来,并使用同步事件分离器等待这些事件的发生。比如说,TCP协议层会使用select同步事件分离器操作来等待客户端发送的数据到达连接的socket handle上。
-
当与某个事件源对应的Handle变为ready状态时,(比如说TCP socket变为等待读状态时),同步事件分离器就会通知Initiation Dispatcher。
-
Initiation Dispatcher会触发事件处理器的回调方法,从而响应这个处于ready状态的Handle。当事件发生时,Initiation Dispatcher会将被事件源激活的Handle作为[key]来寻找并分发恰当的事件处理器回调方法。
-
Initiation Dispatcher会回调事件处理器的handle_event回调方法来执行特定于应用的功能(开发者自己所编写的功能),从而响应这个事件。所发生的事件类型可以作为该方法参数并被该方法内部使用来执行额外的特定于服务的分离与分发。
五、reactor在java领域的应用
1. 传统的java网络编程模型
采用java OIO的网络编程模型,客户端与服务端建立好连接过后,服务端对每一个建立好的连接使用一个handler来处理,而每个handler都会绑定一个线程。
这样做在连接的客户端不多的情况下,也算是个不错的选择。而且连接的客户端很多的情况下就会出现问题。
1) 每一个连接服务端都会产生一个线程,当并发量比较高的情况下,会产生大量的线程。
2) 在服务端很多线程的情况下,大量的线程的上下文切换是一个很大的开销,会比较影响性能。
3) 与服务端连接建立后,连接上未必是时时刻刻都有数据进行传输的,但是创建的线程一直都在,会造成服务端线程资源的一个极大的浪费。
2. 经典的reactor模式设计
由于java OIO的网络编程模型在客户端很多的情况下回产生服务端线程数过多的问题,因此根据reactor模式做出了改进。
根据上图,reactor角色对IO事件进行监听和分发。当事件产生式,reactor会分发给对应的处理器进行处理。OIO存在的一个问题是IO阻塞,因此一个socket开启一个线程来处理以防止一个连接IO的阻塞影响到其他的连接的处理。而这里通过对于IO事件的监听和分发,很好的解决了这个问题。服务端只需要一个IO线程就能处理多个客户端的连接。这就解决了OIO的阻塞问题和多个客户端连接而导致服务端线程数过多的问题。
但是这种模型还是有缺陷的,那就是所有的客户端的请求都由一个线程来进行处理,当并发量比较大的情况下,服务端的处理性能肯定会下降,因为服务端每次只能处理一个请求,其他的请求只能等待。
3. 多线程版本的Reactor模式设计
上面的单线程reactor模式因为服务端只有一个线程处理IO和业务逻辑,服务端性能肯定受到限制。因此就有了多线程版本:
如上图所示,reactor还是一个线程,负责监听IO事件以及分发。只不过业务逻辑处理部分使用了一个线程池来进行处理。这样就解决了服务端单线程处理请求而带来的性能瓶颈。
但是这样还是有问题,这样会把性能的瓶颈转移到IO处理上。因为IO事件的监听和分发采用的还是单个线程,在并发量比较高的情况下,这个也是比较影响性能的。这是否还有继续优化的空间呢?
4. 多reactor的多线程版本
我们知道reactor主要是负责IO事件的监听和分发。单个reactor单个线程这种模式在并发量比较高的情况下,会存在性能瓶颈。那么改进的方案显然就是采用多个reactor。
上图所示的mainReactor和subReactor都可能包含多个Selector(Java NIO),而每个Selector都是跟一个线程绑定的。这样就解决了单个reactor单个线程所带来的性能问题。实际上netty就是采用的这种设计,虽然具体的实现细节有些不一样,但是总体思想是一样的。
六、reactor模式在java NIO和netty中的应用
1. NIO与reactor模式的对应
我们知道,NIO的网络编程中,会有一个死循环执行Selector.select()操作,选择出注册到Selector上的Channel已经准备好的感兴趣的IO事件,然后再对这些事件进行处理。而NIO中的Selector组件对应的就是Reactor模式的同步事件分离器。选择过后得到的SelectionKey,其实就对应的是上面的句柄,也就是代表的一个个的IO事件。而NIO中并没有进行事件分发和封装处理器,因此Reactor模式中的其他组件NIO并没有给出实现。
2. Reactor模式在netty中的应用
上面java NIO实现了reactor模式的两个角色,而剩余的三个角色Netty给出了实现。学习过netty的应当知道,netty服务端的编程有一个bossGroup一个workerGroup,还需要编写自己的ChannelHandler。而bossGroup和workerGroup都是一个事件循环组(EventLoopGroup,一般我们用的是NIOEventLoopGroup),每个事件循环组有多个事件循环(EventLoop,NIO对应的是NIOEventLoop)。而bossGroup中的某一个事件循环就充当了Initiation Dispatcher(初始分发器)的角色,Netty中我们需要实现的Handler的顶层接口ChannelHandler对应的就是Event Handler(事件处理器)角色,而我们添加进去的一个个的Handler对应的就是Concrete Event Handler(具体事件处理器)。
结构上的对应:
Initiation Dispatcher ———— NioEventLoop
Synchronous EventDemultiplexer ———— Selector
Handle———— SelectionKey
Event Handler ———— ChannelHandler
ConcreteEventHandler ———— 具体的ChannelHandler的实现
上面分析的最后一个模型图上的角色对应:
Netty服务端使用了“多Reactor线程模式”
mainReactor ———— bossGroup(NioEventLoopGroup) 中的某个NioEventLoop
subReactor ———— workerGroup(NioEventLoopGroup) 中的某个NioEventLoop
acceptor ———— ServerBootstrapAcceptor
ThreadPool ———— 用户自定义线程池或者EventLoopGroup
七、总结
本博客介绍了reactor模式的理论基础以及reactor在java 在netty中的实现。特别是在netty中,reactor模式得到了很好的实现。而netty也是一个非常优秀的网络通信框架,无论是从框架本身的设计,还是其对于性能上的极致压缩来说,都是非常棒的。后面我会陆续写一些博客来对netty进行介绍。