简介:Reactor 设计模式是一种事件驱动的设计模式,将一个或者多个客户端请求分发到不同的处理器上,来提升事件处理的效率。主要的应用场景就是java NIO当中用户处理网络请求。使用的是异步非阻塞IO
在接受Reactor 模式之前,需要先了解常见的几种IO网络模型。
1、BIO(阻塞IO模型)
以套接字模型为例:在进程空间中调用recvfrom, 其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才可以返回,在此期间会一直等待,进程在从调用recvfrom 开始到它返回的整段事件内都是阻塞的,(举个栗子:我去餐馆吃饭,我先点菜,点完菜之后,我就坐在一旁等待,直到菜做好之后,我再开始吃,在师傅做菜的过程中,我没有做任何事情一直在等待)
2、非阻塞IO模型
recvfrom 从应用层到内核的时候,如果缓冲区没有数据,会直接返回一个信息,然后过段时间之后,再去进行轮询检测这个状态,看是不是有数据到来,
3、I/O复用模型
进程通过将一个或者多个操作阻塞在select上,select可以帮我们检测fd(文件操作符)是否处于就绪状态,如果有数据准备好,就返回可操作的信息,再进行进一步的读写操作
4、异步I/O
提前告知内核启动某个操作,并让内核在整个操作完成后(将数据从内核复制到用户自己的缓冲区)通知我们,
I/O多路复用技术
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而是系统在单线程的情况下可以同时处理多个客户端请求。最大的优势就是系统开销小,不需要创建额外的线程,服务器需要同时处理多个处于监听状态或者多个连接状态的套接字,
要了解IO多路复用,这个是NIO(None-blocking IO)的基础
我们最开始传统(BIO)的处理方式:
每个客户端发送一个请求,服务器就创建一个线程,用来处理上面的操作步骤,但是当请求的数据量达到一定情况之后,线程的开销也是很大的,虽然这种情况可以实现非阻塞的IO请求,但是弊端也是非常明显的
有以下缺点:
随着线程数增加,极大的降低了数据的吞吐量
线程之间进行上下文切换的开销也很大
每个客户端和服务器的连接建立好之后,数据并不是一直都存在,服务器端的线程还处于存活状态,但是一直空闲,这就造成了资源的极大浪费。
因此,单线程的reactor模式就产生了
上面这个流程操作就是:
所有的IO操作都在一个NIO线程上工作,
客户端发起连接之后,acceptor类接受客户端的请求,链路建立成功之后,通过dispatcher把信息发送到指定的handler上面进行处理, 用户线程编码后,通过NIO线程将信息发送给客户端
Acceptor 会在服务器启动之间将相关的事件注册相应的组件上面,用于处理连接
Reactor角色是一个线程,用来检测用户发送来的连接,当连接建立之后,将数据派发给特定的处理器,也就是途中黄色部分。
这里的Reactor 通过调用适当的处理器来响应具体的IO事件
handler用来处理非阻塞的动作,通过绑定处理程序来管理事件。
这样做,不会为每个客户端都创建一个线程,我们只有一个线程来处理客户端的连接,连接建立好之后,就会把相关的操作交给handler 处理,
这样的问题就是:
1、如果handler的处理时间过长,是会拖慢reactor整个线程的执行速度,并不能很好的响应客户端的请求
2、当请求的数量很大的时候,性能上根本无法支撑,cpu处理达到100%,并不能加快响应速度,反而会降低
3、连接数量过多,会导致连接超时,进一步重新发起连接,最终会导致大量的信息积压。
4、一旦线程处于死循环,会导致整个系统无法使用,不能处理外部请求和返回响应信息,导致系统故障
多线程模型:
多线程模型,主要是有一组NIO线程来处理IO操作,
多线程的操作流程是:
有一个专门的NIO线程来监听服务端,接收客户的TCP连接
网络IO操作有一个专门的线程池去处理,
对于信息的读取及相关的操作都是在一个线程中执行,这样避免了并发操作问题
缺点:
一个线程负责监听处理客户端连接可能会造成性能问题,有些业务场景下对于连接可能会比较耗时,比如安全认证之类的
主从Reactor模型:
这种主从reactor的处理方式是:
服务端用来接受客户端连接请求的不再是一个NIO线程,而且是一个NIO线程池;
mainReactor是由NIO线程池组成的,主要是用来监听客户端连接,连接建立之后,会将创建的SocketChannel注册到subReactor(也是一个NIO线程池)上,在这个线程上负责编码,解码,数据处理等一系列操作,
同时,mainReactor 也使用线程池或者多个线程(少于客户端连接数量的线程数),用来处理IO请求,将读取到的数据,转交给subReactor, 在subReactor中通过线程池来处理这些非IO的操作。
其实这个模式在日志服务器模型中也是有使用到的
上图是客户端发送请求到服务器,我们来
看下处理过程
服务器端,会先将处理器注册到初始分发器(Initiation Dispatcher)上面
服务器端调用事件处理方法(handle_events),然后进行启动
服务器端一直阻塞在select方法上面
客户端发送请求到服务器端,select方法就会返回
handle_event方法通知初始分发器
acceptor 接受到客户端的请求
acceptor 创建一个handler 来处理客户端的请求
将handler注册到初始分发器上面
以上只是在客户端向服务器建立连接时的操作步骤,接下来是连接建立好之后,数据处理流程:
客户端发送数据到服务器,
服务器的初始分发器检测到事件发生,根据返回的handle(句柄),
遍历所有的处理器,找到与这个事件关联的处理器,
然后处理器对数据进行处理,
最后将处理好的数据返回给客户端。
在reactor 模式中一共有以下五个角色
Handle: 表示的是一个句柄或者描述符,本质上是一种资源,由操作系统提供,该资源用于表示一个个的事件,比如文件描述符,在网络服务情况下,就是网络编程中的socket 描述符。这个事件既可以来自内部也可以来自外部,外部主要是指客户端的连接请求,客户端发送过来的数据等;内部的话,指的是操作系统产生的定时任务等。本质上是一个文件描述符
Synchronous Event Demultiplexer(同步事件分离器):用于等待事件的发生,调用方在调用的时候会发生阻塞,一直阻塞到同步事件分离器上游事件发生为止。对于linux系统来说,同步事件分离器指的就是I/O多路复用机制,比如select, poll, epoll 等,映射到java NIO 上就是selector 组件。
Event Handler(事件处理器):这个角色就是用多个回调方法组成的,这些回调方法构成了与应用相关的某个事件的反馈,netty 当中提供了大量的事件处理器,比如InboundHanler, 事件对应的回调就是,注册通道,取消通道,主要是用于在特定事件产生时进行逻辑处理。
Concrete Event Handler (具体的事件处理器):这个处理器主要是开发者自己去编写的,是事件处理器的具体实现,和业务逻辑相关,
Initiation Dispatcher(初始分发器):主要作用是添加EventHandler, 删除 EventHandler, 以及派发事件到 EventHandler, 通过同步分离器等待事件的发生,一旦发生之后,出事分发器就会分离出一个事件,然后调用事件处理器,最后调用该处理器的相关方法。
整个模型的执行流程是:
应用回向Initiation Dispatcher 注册具体的事件处理器,应用会标识出,当某个事件发生时,通知该处理器,这个事件是与handle关联的
Initiation Dispatcher 会要求每个事件处理器向其传递内部的handle,
当所有的处理器都注册结束之后,应用会调用handle_event 来启动 Initiation Dispatcher的事件循环。这时Initiation Dispatcher 会将每个注册的事件管理器的handle合并起来,并使用同步分离器等待事件的发生。比如说,TCP协议层会使用select 同步事件分离器操作来等待客户端发送的数据到达连接的socket handle 上
当与某个事件源对应的handle变为ready状态时,比如说,TCP socket变为等待读状态时,同步事件分离器就会通知Initiation Dispatcher。
Initiation Dispatcher 就会触发事件处理器的回调方法,从而来响应这个已经处于ready状态的事件、
Initiation Dispatcher回调处理器 handle_event 的具体方法,来执行特定的业务功能。
netty 当中的线程模型
对于以上的线程模型,netty都可以通过配置相应的参数来实现,
netty中的reactor模型的使用比较类似主从reactor模型,在服务端的启动代码,会创建一个bossGroup,workerGroup
bossGroupl类似于mainReactor
workerGroup 类似于 subReactor
ServerBootstrapAcceptor 对应acceptor
concrete Event Hanler 就是用户自己编写的处理器