
一、背景
看过之前文章的同学都知道,Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
今天我们聊一聊Reactor和Proactor模式,阅读本文尽量之前,尽量先看看之前的文章,方便理解,链接如下:
二、网络编程
所有的网络服务,都有五个最基础的部分:
读请求(read request)
读解析(read decode)
处理程序(process service)
应答编码 (encode reply)
发送应答(send reply)
接下来我们要做的事情就是找到一种解决这个问题的最佳实践。
传统解决方式

每当来一个请求,都开启一个线程去处理,就是之前讲IO的时候,BIO的处理方式,但是存在下面问题:
当客户端多,并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费
高可用和高性能
实例:Apache网络通信模型(并发量不高,各个IO处理相互隔离)

频繁创建线程拉低系统效率、操作系统限制(fd数量限制),所以,现在Apache在大型网络应用中几乎看不到身影,完全被替代掉了。
要解决上面的问题,我们也可以思考一下?
一般我们设计一个事件处理模型的程序有两种思路
1、轮询方式
线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。
2、事件驱动方式
发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。
下面看一个事件驱动模型图:

主要包括4个基本组件:
事件队列(Event Queue):待处理的事件存储队列
分发器(Event Mediator):将不同的事件分发到不同的业务逻辑单元
事件通道(Event Channel):分发器与处理器之间的联系渠道
事件处理器(Event Processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作
三、Reactor模式
Reactor模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。
服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。
在Reactor模式中,包含如下角色
Reactor 将I/O事件发派给对应的Handler
Acceptor 处理客户端连接请求
Handlers 执行非阻塞读/写
根据Reactor的数量和处理资源池线程的数量不同,有3种:
1、单Reactor单线程

方案说明:
Reactor对象通过select监控客户端请求事件,收到事件后通过dispatch进行分发
如果是建立连接请求事件,则由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理
如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应
Handler会完成read->业务处理->send的完整业务流程
优点:
模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
缺点:
性能问题:只有一个线程,无法完全发挥多核CPU的性能
Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈可靠性问题:线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
实例:Redis通信模型(并高发,高吞吐量)

客户端的数量有限,业务处理非常快速,业务处理的时间复杂度O(1),多路复用,单线程基于内存操作避免线程上下文切换的开销。
redis的劣势也比较明显,无法发挥多核CPU的性能,不过可以通过多个实例来完善。另外就是可靠性,这个可以使用主从、哨兵、集群等模式来完善
2、单Reactor多线程

方案说明
Reactor对象通过select监控客户端请求事件,收到事件后通过dispatch进行分发
如果是建立连接请求事件,则由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的续各种事件
如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应
Handler只负责响应事件,不做具体业务处理,通过read读取数据后,会分发给后面的Worker线程池进行业务处理
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
Handler收到响应结果后通过send将响应结果返回给client
优点
可以充分利用多核CPU的处理能力
缺点
多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
Reactor承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能
实例:一时没想到。。。
3、主从Reactor多线程

方案说明
Reactor主线程MainReactor对象通过select监控建立连接事件,收到事件后通过Acceptor接收,处理建立连接事件
Acceptor处理建立连接事件后,MainReactor将连接分配Reactor子线程给SubReactor进行处理
SubReactor将连接加入连接队列进行监听,并创建一个Handler用于处理各种连接事件
当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应
Handler通过read读取数据后,会分发给后面的Worker线程池进行业务处理
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
Handler收到响应结果后通过send将响应结果返回给client
优点
父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
父线程与子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据
实例:Nginx通信模型(并高发,高吞吐量)

多路复用,多进程处理,避免单个IO阻塞耗时,引起全局所有IO等待 。
这里稍微思考一下,Nginx为啥使用多进程而不是多线程?
实例:Netty

相同点:基于与nginx类似,多任务(nginx多进程,netty线程池)、多路复用
不同点:nginx Work进程主动式抢占客户端socket处理,netty则是accept线程池基于负载均衡策略向IO线程池分发任务
四、Proactor模型
在Reactor模式中,Reactor等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),然后把这个事件传给事先注册的Handler(事件处理函数或者回调函数),由后者来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以Reactor是非阻塞同步网络模型。如果把I/O操作改为异步,即交给操作系统来完成就能进一步提升性能,这就是异步网络模型Proactor

Proactor是和异步I/O相关的,详细方案如下:
ProactorInitiator创建Proactor和Handler对象,并将Proactor和Handler都通过AsyOptProcessor(Asynchronous Operation Processor)注册到内核
AsyOptProcessor处理注册请求,并处理I/O操作
AsyOptProcessor完成I/O操作后通知Proactor
Proactor根据不同的事件类型回调不同的Handler进行业务处理
Handler完成业务处理
可以看出Proactor和Reactor的区别:Reactor是在事件发生时就通知事先注册的事件(读写在应用程序线程中处理完成);Proactor是在事件发生时基于异步I/O完成读写操作(由内核完成),待I/O操作完成后才回调应用程序的处理器来处理进行业务处理
理论上Proactor比Reactor效率更高,异步I/O更加充分发挥DMA(Direct Memory Access,直接内存存取)的优势,但是有如下缺点:
编程复杂性
由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以Debug内存使用
缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比Reactor模式,在socket已经准备好读或写前,是不要求开辟缓存的操作系统支持
Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux2.6才引入,目前异步I/O还不完善
因此在Linux下实现高并发网络编程都是以Reactor模型为主。
写到这里感觉剩下内容还是较多,一篇写不完,决定再写个下篇,内容是Netty里面如何使用的Reactor模型的。最近一段时间较忙,更新有点慢
参考:
Scalable IO in Java.PDF
https://www.jianshu.com/p/2965fca6bb8f
http://www.jasongj.com/java/nio_reactor/
另外文中有错误的地方,请帮忙指正,多谢!




