1 Reactor模式简介
Reactor模式由Reactor线程、Handlers处理器两个角色组成,两个角色的职责分别是:
1、Reactor线程的职责:负责相应IO事件,并且分发到Handlers处理器。
2、Handlers处理器的职责:非阻塞的执行业务处理逻辑。
从上面的Reactor模式定义中看不出这种模式有什么神奇的地方。当然,从简单到复杂,Reactor模式也有很多版本,前面的定义仅仅是最为简单的一个版本。如果需要彻底了解Reactor模式,还得从最原始的OIO编程开始讲起。
1.1 多线程OIO的致命缺陷
在Java的OIO变成中,原始的网络服务器程序一般使用一个while玄幻不断地监听端口是否有新的连接。如果有,就调用一个处理函数来完成传输处理。示例代码如下:
while(true){
socket = accept();//阻塞,接收连接
handler(socket);//读取数据,业务处理等
}
这种方法的最大问题是:如果前一个网络连接的handler(socket)没有处理完,那么后面的新连接无法被服务端接收,于是后面的请求就会被阻塞,导致服务器的吞吐量太低。这对于服务器来说是一个严重的问题。
为了解决这个严重的连接阻塞问题,出现了一个经典的模式:Connection Per Thraed (一个线程处理一个连接)模式。即对于每一个新的网络连接都分配一个线程。每个线程独自处理自己负责的socket连接的输入和输出,任何socket连接的输出和输入处理都不会阻塞到后面新socket连接的监听和建立,这样服务器的吞吐量就得到了提升。早起版本的Tomcat服务器就是这样实现的。
Connection Per Thraed模式的优点是解决了前面的新连接被严重阻塞的问题,在一定程度上较大地提高了服务器的吞吐量。
Connection Per Thraed 模式的缺点是对于大量的连接,需要耗费大量的线程资源,对线程资源要求太高。在系统中,线程是比较昂贵的系统资源。如果线程的数量太多,系统将无法承受。而且,线程的反复创建、销毁、切换也需要代价。因此,在高并发的应用场景下,多线程OIO的缺陷是致命的。
新的问题来了:如果减少线程数,让一个线程同时负责处理多个socket的输出和输入,行必行?实际上是不行的。因为在传统的OIO编程中,每一次socket传输的IO读写处理都是阻塞的。在同一时刻,一个线程里只能处理一个socket的读写操作,前一个socket操作被阻塞了,其他连接的OIO操作同样无法被并行处理。所以,在OIO中,即使是一个线程同时负责处理多个socket连接的输入和输出,同一时刻也只能处理一个连接的IO操作。
如果解决Connection Per Thraed模式的巨大缺陷呢?一个有效途径是使用Reactor模式。用Reactor模式对线程的数量进行控制,做到一个线程处理大量的连接。首先来看一个简单版本——单线程的Reactor模式。
2 单线程Reactor模式
总体来说,Reactor模式有点类似事件驱动模式。在事件驱动模式中,当有事件触发时,事件源会将事件分发到Handler(处理器),由Handler负责事件处理。Reactor模式中的反应器角色类似事件驱动模式中的事件分发器角色。
具体来说,在Reactor模式中有Reactor和Handler两个重要角色。
1、Reactor:负责查询IO事件,当检测到一个IO事件时将其发送到相应的Handler处理器去处理。这里的IO事件就是NIO中选择器查询出来的通道IO事件。
2、Handler:与IO事件绑定,负责IO事件的处理,完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写入通道等。
2.1 什么是单线程Reactor
什么是单线程版本的Reactor模式?简单地说,Reactor和Handlers处于一个线程中执行。这是最简单的Reactor模式。如下图:
基于Java NIO如何实现简单的单线程版本的 Reactor模式呢?需要用到SelectionKey(选择键)的几个重要的成员方法:
1、void attach(Object o):将对象附件到选择键。
此方法可以将任何Java POJO对象作为附件添加到SelectionKey实例。此方法非常重要,因为在单线程版本的Reactor模式实现中可以将Handler实例作为附件添加到SelectionKey实例。
2、Object attachment():从选择键获取附加对象。
此方法与attach()是配套使用的,起作用是取出之前通过attach(Object o)方法添加到SelectionKey实例的附加对象。这个方法同样非常重要,当IO事件发生时,选择键将被select方法查询出来,可以直接将选择键的附件对象取出。
在Reactor模式实现中,通过attachment()方法所取出的是之前通过attach()方法绑定的Handler实例,然后通过该Handler实例完成相应的传输处理。
总之,在Reactor模式中,需要将attach和attachment结合使用:在选择键注册完成之后调用attach()方法,将Handler实例绑定到选择键中;当IO事件发生时调用attachment()方法,可以从选择键中取出Handler实例,将事件分发到Handler处理器中完成业务处理。
2.2 单程Reactor模式参考代码
public class EchoServerReactor implements Runnable{
Selector selector;
ServerSocketChannel serverSocketChannel;
EchoServerReactor() throws IOException{
//Reactor初始化
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8080);
//非阻塞
serverSocketChannel.configureBlocking(false);
//分步处理,1.接收accept事件
SelectionKey sk = serverSocketChannel.register(selector, 0, new AcceptorHandler());
serverSocketChannel.socket().bind(address);
System.out.println("服务端开始监听:"+address);
sk.interestOps(SelectionKey.OP_ACCEPT);
}
class AcceptorHandler implements Runnable{
@Override
public void run() {
try {
SocketChannel channel = serverSocketChannel.accept();
System.out.println("接收到一个连接");
if(channel != null){
new EchoHandler(selector,channel);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
//io事件的查询
// 限时阻塞查询
selector.select(1000);
Set<SelectionKey> selected = selector.selectedKeys();
if (null == selected || selected.size() == 0) {
continue;
}
Iterator<SelectionKey> it = selected.iterator();
while (it.hasNext()) {
//Reactor负责dispatch收到的事件
SelectionKey sk = it.next();
it.remove(); //避免下次重复处理
dispatch(sk);
}
// selected.clear();
}
}catch (IOException e){
e.printStackTrace();
}
}
void dispatch(SelectionKey sk) {
Runnable handler = (Runnable) sk.attachment();
//调用之前attach绑定到选择键的handler处理器对象
if (handler != null) {
handler.run();
}
}
}
在上面的的代码中设计了一个Handler,叫AcceptorHandler处理器,是一个内部类。在注册serverSocket服务监听连接的接收事件之后,创建一个AcceptorHandler新连接处理器的实例作为附件,被附加(attach)到SelectionKey中。当新连接事件发生后,取出之前附加到 SelectionKey中的Handler业务处理器进行socket的各种IO处理。处理器AcceptorHandler的两个职责是完成新连接的接收工作、为新连接创建一个负责数据传输的Handler(IOHandler)。顾名思义,IOHandler就是负责socket连接的数据输入、业务处理、结果输出。
public class EchoHandler implements Runnable{
final SocketChannel channel;
final SelectionKey sk;
final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
static final int RECEVING = 0, SENDING = 1;
int state = RECEVING;
EchoHandler(Selector selector,SocketChannel c) throws IOException{
channel = c;
c.configureBlocking(false);
sk = channel.register(selector,0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
selector.wakeup();
}
@Override
public void run() {
try {
if(state == SENDING){
channel.write(byteBuffer);
byteBuffer.clear();
sk.interestOps(SelectionKey.OP_READ);
state = RECEVING;
}else if(state == RECEVING){
int length = 0;
while((length = channel.read(byteBuffer)) > 0){
System.out.println("buteBuffer.array.size = " + length);
}
byteBuffer.flip();
sk.interestOps(SelectionKey.OP_WRITE);
state = SENDING;
}
}catch (Exception e){
e.printStackTrace();
sk.cancel();
try {
channel.finishConnect();
}catch (IOException ioe){
ioe.printStackTrace();
}
}
}
}
IOHandler.java源码
public class IOHandler implements Runnable{
SocketChannel channel = null;
SelectionKey sk = null;
IOHandler (Selector selector,SocketChannel c) throws IOException {
channel = c;
c.configureBlocking(false);
//与之前的注册方式不同,先仅仅取得选择键,之后在单独设置感兴趣的IO事件
//先取到选择键
sk = channel.register(selector,0);
//将Handler处理器作为选择键的附件
sk.attach(this);
//注册读写就绪事件
sk.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
@Override
public void run() {
//处理业务
}
}
在传输处理器IOHandler的构造器中,有两点比较重要:
1、将新的 SocketChannel 传输通道注册到Reactor类的同一个选择中。这样保证了Reactor在查询IO事件时能查询到Handler注册到选择器的IO事件。
2、Channel传输通道注册完成后,将IOHandler实例自身作为附件附加到选择键中。这样,在Reactor类分发事件(选择键)时,能执行到IOHandler的run()方法,完成数据局传输处理。
3 单线程Reactor模式的EchoServer的实战案例
EchoServer的功能很简单:读取客户端的输入并回显到客户端,所以也叫回显服务器。基于Reactor模式来实现,涉及三个重要的类。
1、设计一个反应器类:EchoServerReactor类。
2、设计两个处理器类:AcceptorHandler新连接处理器、EchoHandler回显处理器。
EchoServerReactor.java ,AcceptorHandler.java 源码
import com.crazymakercircle.util.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class EchoServerReactor implements Runnable{
Selector selector;
ServerSocketChannel serverSocketChannel;
EchoServerReactor() throws IOException{
//Reactor初始化
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8080);
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//分步处理,第一步,接收accept事件
SelectionKey sk = serverSocketChannel.register(selector, 0, new AcceptorHander());
serverSocketChannel.socket().bind(address);
Logger.info("服务端开始监听:",address);
sk.interestOps(SelectionKey.OP_ACCEPT);
}
@Override
public void run() {
try {
while(!Thread.interrupted()){
//io事件的查询,限制阻塞查询
selector.select(1000);
Set<SelectionKey> selected = selector.selectedKeys();
if(null == selected || selected.size() == 0){
continue;
}
Iterator<SelectionKey> it = selected.iterator();
while(it.hasNext()){
//Reactor负责dispatch收到的事件
SelectionKey sk = it.next();
it.remove();
dispatch(sk);
}
}
}catch (IOException e){
e.printStackTrace();
}
}
void dispatch(SelectionKey sk){
Runnable handler = (Runnable) sk.attachment();
//调用之前 attach绑定到选择键的handler处理器对象
if(handler != null){
handler.run();
}
}
//Handler:新连接处理器
class AcceptorHander implements Runnable{
@Override
public void run() {
try {
SocketChannel channel = serverSocketChannel.accept();
Logger.info("接收到一个连接");
if(channel != null){
new EchoHandler(selector,channel);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new Thread(new EchoServerReactor()).start();
}
}
EchoHandler.java 源码
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class EchoHandler implements Runnable{
SocketChannel channel = null;
SelectionKey sk = null;
final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
static final int RECIEVINT = 0,SENDING = 1;
int state = RECIEVINT;
EchoHandler(Selector selector,SocketChannel c) throws IOException{
channel = c;
//设置为非阻塞
c.configureBlocking(false);
//先取到选择键,绑定事件处理器
//后设置感兴趣的IO事件
sk = channel.register(selector,0);
//将Handler作为选择键的附件
sk.attach(this);
//第二步,注册read就绪事件
sk.interestOps(SelectionKey.OP_READ);
//唤醒事件查询线程,在单线程模式下,这里没有意义
selector.wakeup();
}
@Override
public void run() {
try {
if(state == SENDING){
//发送状态,写入通道
channel.write(byteBuffer);
//写完后,准备开始从通道读,bytebuffer切换写模式
byteBuffer.clear();
//写完后,注册read就绪事件
sk.interestOps(SelectionKey.OP_READ);
//写完后,进入接收状态
state = RECIEVINT;
}else if(state == RECIEVINT){
//开始的时候,为接收状态
//从通道读
int length = 0;
while((length = channel.read(byteBuffer)) > 0){
Logger.info(new String(byteBuffer.array(),0,length));
}
//读完后,准备开始写入通道,切换成写模式
byteBuffer.flip();
//读完后,注册write就绪事件
sk.interestOps(SelectionKey.OP_READ);
//读完后,进入发送的状态
state = SENDING;
}
}catch (IOException e){
e.printStackTrace();
sk.cancel();
try {
channel.finishConnect();
}catch (IOException ex){
ex.printStackTrace();
}
}
}
}
4 单线程Reactor模式的缺点
单线程Reactor模式是基于Java的NIO实现的。相对于传统的多线程OIO,Reactor模式不再需要启动成千上万的线程,避免了线程上下文的频繁切换,服务端的效率自然是大大提升了。
在单线程Reactor模式中,Reactor和Handler都在同一个线程中执行。这样,带来了一个问题:当其中某个Handler阻塞时,会导致其他所有的Handler都无法执行。在这种场景下,被阻塞的Handler不仅仅负责输入和输出处理的处理器,还包括负责新连接监听的AcceptHandler处理器,可能导致服务器无响应。这是一个非常严重的缺陷导致单线程反应器模型在生产场景中使用的比较少。
除此之外,目前的服务器都是多核的,单线程Reactor模式模型不能充分利用多核资源。总之,在高性能服务器应用场景中,单线程Reactor模式实际用的很少。