一. 多Reactor模型
1.1最优的解决方案
当我们经历了最初的Reactor模型,实现了单线程基于Selector选择器的NIO通信模式,到升级为多线程Reactor模型,由一个Selector实现事件的分发到不同的线程进行服务器的操作,最后我们找到了一种最优化的解决方案——多Reactor模型。
从图中可以直接看出,我们实现了一个主反应器(MainReactor)和多个次反应器(subReactor),主反应器中包含了一个ServerSocketChannel
用来接收所有的请求,主反应器中的选择器通过事件循环(EventLoop),交由acceptor
进行分发到不同的次反应器,次反应器维护主反应器交付的SocketChannel
,其中的选择器通过事件循环,来进行read
和write
事件的派发。
1.2模型代码
1.2.1主反应器
public class NioMainReactor {
// 主反应器中的选择器
private Selector reactorSelector;
//主反应器中的服务通道
private ServerSocketChannel serverSocketChannel;
// 次反应器数组
private NioSubReactor[] subReactor;
// cpu核心数
int coreNum;
/**
* 主反应器构造函数
* 负责确定监听端口,打开选择器,打开通道,初始化次反应器数组
*
* @param port
* @throws IOException
*/
public NioMainReactor(int port) throws IOException {
reactorSelector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(reactorSelector, SelectionKey.OP_ACCEPT);
serverSocketChannel.bind(new InetSocketAddress(port));
// 获得当前机器环境的cpu核心数量
coreNum = Runtime.getRuntime().availableProcessors();
subReactor = new NioSubReactor[coreNum];
for(int i =0; i<subReactor.length;i++) {
subReactor[i] = new NioSubReactor();
}
}
/**
* 主反应器的事件循环,用于接收外界的socket请求,并分发给次选择器
*
* @throws IOException
*/
private void selectLoop() throws IOException {
int index = 0;
while(true) {
reactorSelector.select();
Set<SelectionKey> selectionKeys = reactorSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
// 负载均衡的策略是轮流分发任务
index = (index++)%coreNum;
accept(serverSocketChannel,index);
}
selectionKeys.remove(selectionKey);
}
}
}
/**
* 负责将接收到的SocketChannel分发给次反应器
*
* @param serverSocketChannel
* @param index
* @throws IOException
*/
private void accept(ServerSocketChannel serverSocketChannel,int index) throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
socketChannel.configureBlocking(false);
// 分发给subReactor处理
NioSubReactor currentSubReactor = subReactor[index];
currentSubReactor.addSocketChannel(socketChannel);
currentSubReactor.wakeup();
}
}
}
1.2.2次反应器
public class NioSubReactor {
//次反应器中的选择器
private Selector subReactorSelector;
// 当前机器环境中CPU核心数量
private static int coreNum = Runtime.getRuntime().availableProcessors();
// 静态线程池,每一条线程对应一个次反应器
private static Executor pool = Executors.newFixedThreadPool(coreNum);
public NioSubReactor() throws IOException {
subReactorSelector = Selector.open();
}
/**
* 添加通道到该次反应器
* 注册到次反应器的选择器中
* 同时创建该通道的处理器类
*
* @param socketChannel
* @throws ClosedChannelException
*/
public void addScocketChannel(SocketChannel socketChannel) throws ClosedChannelException {
// 主选择器传入的通道注册到次次反应器
SelectionKey selectionKey = socketChannel.register(subReactorSelector, SelectionKey.OP_READ);
selectionKey.attach(new Handler(socketChannel));
}
public void wakeup() {
// 唤醒因为选择器选择阻塞的方法
subReactorSelector.wakeup();
}
/**
* 事件循环
*
* @throws IOException
*/
public void selectLoop() throws IOException {
while(true) {
subReactorSelector.select();
Set<SelectionKey>selectionKeys = subReactorSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
// 获得绑定的处理器
Handler handler = (Handler)selectionKey.attachment();
if (selectionKey.isReadable()) {
handler.read();
}else if (selectionKey.isWritable()) {
handler.write();
}
}
selectionKeys.clear();
}
}
/**
* 通道数据处理内部类
*
* @author CringKong
*
*/
class Handler{
private SocketChannel socketChannel;
public Handler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public void read() {
// 数据处理后执行socketChannel.read();
}
public void write() {
// 数据处理后执行socketChannel.write();
}
}
}
1.3模型代码分析
多Reactor模式其实就是多个单Reactor模式的组合模式,而且每个Reactor都是同步阻塞的模型,为了说明这一点,我们先来看主反应器的核心代码——事件循环。
private void selectLoop() throws IOException {
int index = 0;
while(true) {
reactorSelector.select();
Set<SelectionKey> selectionKeys = reactorSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
// 负载均衡的策略是轮流分发任务
index = (index++)%coreNum;
accept(serverSocketChannel,index);
}
selectionKeys.remove(selectionKey);
}
}
}
- 同步:因为是单线程顺序执行,每一次
accept(serverSocketChannel,index)
都是在循环中顺序执行的,确保已经完成方法以后进行下一步操作。 - 阻塞:
reactorSelector.select()
方法调用以后会阻塞线程,直到有下一个连接接入,但在高并发的服务器情况下,几乎是没有阻塞的,因为待处理任务队列始终会有任务存在。
对于次选择器,我们可以使用异步处理的模式,也就是说和我们之前的多线程Reactor模型一样,每次都将对于通道数据的处理(I/O过程),启动一个单独的线程,但这样做有一些问题,需要很好的处理才能防止通道被重复选择(因为另外一个线程处理数据的同时,选择器会认为该通道依旧处于可读/写状态,所以会重复选择)。
所以对于次选择器,我们采用了同步非阻塞的模型来进行数据的处理,我们来看一下次选择器的核心代码——事件循环和处理器。
public void selectLoop() throws IOException {
while(true) {
subReactorSelector.select();
Set<SelectionKey>selectionKeys = subReactorSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
Handler handler = (Handler)selectionKey.attachment();
if (selectionKey.isReadable()) {
handler.read();
}else if (selectionKey.isWritable()) {
handler.write();
}
}
selectionKeys.clear();
}
}
对于次选择器的事件循环,有以下几点:
1. Handler handler = (Handler)selectionKey.attachment()
,用来获取相应通道的处理器对象,因为每个通道都唯一绑定了一个自己的处理器对象。
2. 根据事件类型的不同,进行不同的任务派派发selectionKey.isReadable()
以及selectionKey.isWritable()
验证当前通道的准备好的操作类型,然后通过调用处理器的方法来进行操作。
3. 处理器的read(
)和write()
方法是非阻塞的,也就是说直接就可以进行数据的读写操作,而不需要等待操作系统内核的数据准备工作,这也是NIO比起BIO高效的原因。
class Handler{
private SocketChannel socketChannel;
public Handler(SocketChannel socketChannel) {
this.socketChannel = socketChannel;
}
public void read() {
// 数据处理后执行socketChannel.read();
}
public void write() {
// 数据处理后执行socketChannel.write();
}
这只是一个模型版的处理器,真实的处理器需要进行判断传输是否完成,编码解码,数据处理,协议封装等等功能,但其核心的任务依旧是read和write数据。
二. 总结
多反应器模型是一种实用的模型,Jboss-netty框架就是采用了这种模型。
- 这种模型相较于单Reactor模型极大的提高了对于高并发处理的性能,而且对于次选择器的数量,取了当前机器可用的CPU核心数,最大化的利用了多核优势。
- 相较于多线程Reactor模型,多Reactor模型避免了一些设计上的缺陷,同步的设计配合上非阻塞式的I/O模型,效率也是十分高的。