前言
上一篇Netty的博客,说明了BIO与NIO,这两者最大的差别提现在多路IO复用上。
什么是reactor模式
reactor模式是一种典型的事件驱动模式,同时在之前的BIO实例,详细解读了BIO的阻塞问题。如果想要通过文字理解Reactor模式,确实很困难。如果通过代码在理解了NIO的基础上,再来看Reactor模式就相对容易一些。所以在理解Reactor模式之前最好需要理解NIO的详细实例,理解Channel与Selector的注册关系。如果要在这个阶段弄懂Reactor模式是什么,这里推荐这篇博文:什么是reactor模式
单线程的reactor模式
在reactor模式中有最重要的两个组件,一个是Reactor反应器(这个就是Selector)另一个是Handler处理器,反应器发现某一个事件注册好了之后,就将事件发送给相应的Handler处理器。一张图或许更容易理解:
上面这幅图就比较好理解了,针对客户端请求,服务端首先注册到Selector上,然后根据注册的事件类型将任务分发给不同的Handler,如果是连接就绪则将其分发给连接成功的处理类(Acceptor) 如果是业务的读写请求,则将其分发给业务处理的Handler。
同时还需要知道的是:SelectorKey中有两个重要的方法
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
public final Object attachment() {
return attachment;
}
attach方法其实就是将处理器附加到指定的SelectionKey上,而attachment方法即获取与SelectionKey上绑定的处理类。
服务端的代码:
package com.learn.reactor.singlereactormodel;
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.util.Iterator;
import java.util.Set;
/**
* autor:liman
* createtime:2019/10/15
* comment:单Reactor线程模型的服务端实现
* Reactor——负责监听就绪事件,并对事件进行分发处理
*/
public class Reactor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public Reactor(int port) throws IOException {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
//将ServerSocketChannel注册到selector上,订阅ACCEPT事件
SelectionKey keys = serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//绑定连接建立的处理对象
keys.attach(new ServerAcceptor(serverSocketChannel,selector));
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
selector.select();//就绪事件到达之前阻塞
Set selected = selector.selectedKeys();
Iterator iterator = selected.iterator();
while(iterator.hasNext()){
//进行任务的分发
dispatch((SelectionKey)iterator.next());
}
selected.clear();
}
}catch (Exception e){
e.printStackTrace();
}
}
private void dispatch(SelectionKey key) {
//这里并没有做区分,任务的分发依旧交给了服务器主线程去处理。
Runnable runThread = (Runnable) key.attachment();
if(runThread!=null){
runThread.run();
}
}
public static void main(String[] args) throws IOException {
new Thread(new Reactor(8888)).start();
}
}
上述代码最核心的其实就是任务的分发。
连接建立的处理对象
package com.learn.reactor.singlereactormodel;
import lombok.extern.slf4j.Slf4j;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* autor:liman
* createtime:2019/10/15
* comment:
*/
@Slf4j
public class ServerAcceptor implements Runnable{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;
public ServerAcceptor(ServerSocketChannel serverSocketChannel, Selector selector) {
this.selector = selector;
this.serverSocketChannel = serverSocketChannel;
}
@Override
public void run() {
SocketChannel socketChannel;
try{
socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){
log.info("收到来自{}的连接",socketChannel.getRemoteAddress());
//处理SocketChannel的读写业务
new ServerHandler(socketChannel,selector);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
真正的业务处理类:
package com.learn.reactor.singlereactormodel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
/**
* autor:liman
* createtime:2019/10/15
* comment:
*/
@Slf4j
public class ServerHandler implements Runnable {
private final SelectionKey selectionKey;
private final SocketChannel socketChannel;
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer writeBuffer = ByteBuffer.allocate(2048);
private final static int READ = 0;
private final static int SEND = 1;
private int status = READ;
public ServerHandler(SocketChannel socketChannel, Selector selector) throws IOException {
this.socketChannel = socketChannel;
this.socketChannel.configureBlocking(false);
selectionKey = socketChannel.register(selector, 0);
selectionKey.attach(this);//将读写的处理类绑定为当前的业务类。
selectionKey.interestOps(SelectionKey.OP_READ);
// selector.wakeup();
}
@Override
public void run() {
try {
switch (status) {
case READ:
read();
break;
case SEND:
send();
break;
default:
}
} catch (IOException e) { //这里的异常处理是做了汇总,常出的异常就是server端还有未读/写完的客户端消息,客户端就主动断开连接,这种情况下是不会触发返回-1的,这样下面read和write方法里的cancel和close就都无法触发,这样会导致死循环异常(read/write处理失败,事件又未被cancel,因此会不断的被select到,不断的报异常)
System.err.println("read或send时发生异常!异常信息:" + e.getMessage());
selectionKey.cancel();
try {
socketChannel.close();
} catch (IOException e2) {
System.err.println("关闭通道时发生异常!异常信息:" + e2.getMessage());
e2.printStackTrace();
}
}
}
private void read() throws IOException {
if (selectionKey.isValid()) {
readBuffer.clear();
int count = socketChannel.read(readBuffer); //read方法结束,意味着本次"读就绪"变为"读完毕",标记着一次就绪事件的结束
if (count > 0) {
// try {
// Thread.sleep(5000L);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
log.info("收到来自客户端的信息,信息为:{}", BufferUtils.readByteBufferInfo(readBuffer));
status = SEND;
selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写方法
} else {
//读模式下拿到的值是-1,说明客户端已经断开连接,那么将对应的selectKey从selector里清除,否则下次还会select到,因为断开连接意味着读就绪不会变成读完毕,也不cancel,下次select会不停收到该事件
//所以在这种场景下,(服务器程序)你需要关闭socketChannel并且取消key,最好是退出当前函数。注意,这个时候服务端要是继续使用该socketChannel进行读操作的话,就会抛出“远程主机强迫关闭一个现有的连接”的IO异常。
selectionKey.cancel();
socketChannel.close();
System.out.println("read时-------连接关闭");
}
}
}
private void send() throws IOException {
if (selectionKey.isValid()) {
writeBuffer.clear();
String returnMessage = String.format("服务端收到来自%s的信息:%s, 200ok;",
socketChannel.getRemoteAddress(),
new String(readBuffer.array()));
writeBuffer.put(returnMessage.getBytes());
log.info("服务端发送消息,消息为:{}", returnMessage);
writeBuffer.flip();
int count = socketChannel.write(writeBuffer); //write方法结束,意味着本次写就绪变为写完毕,标记着一次事件的结束
if (count < 0) {
//同上,write场景下,取到-1,也意味着客户端断开连接
selectionKey.cancel();
socketChannel.close();
log.info("发送消息时连接关闭");
System.out.println("send时-------连接关闭");
}
//没断开连接,则再次切换到读
status = READ;
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
客户端代码
package com.learn.reactor.singlereactormodel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* autor:liman
* createtime:2019/10/15
* comment:
*/
public class NIOClient implements Runnable {
private Selector selector;
private SocketChannel socketChannel;
public NIOClient(String ip, int port) {
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(ip, port));
SelectionKey selectionKey = socketChannel.register(selector, SelectionKey.OP_CONNECT);
selectionKey.attach(new Connector(socketChannel, selector));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
selector.select(); //就绪事件到达之前,阻塞
Set selected = selector.selectedKeys(); //拿到本次select获取的就绪事件
Iterator it = selected.iterator();
while (it.hasNext()) {
//这里进行任务分发
dispatch((SelectionKey) (it.next()));
}
selected.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
void dispatch(SelectionKey k) {
Runnable r = (Runnable) (k.attachment()); //这里很关键,拿到每次selectKey里面附带的处理对象,然后调用其run,这个对象在具体的Handler里会进行创建,初始化的附带对象为Connector(看上面构造器)
//调用之前注册的callback对象
if (r != null) {
r.run();
}
}
public static void main(String[] args) {
new Thread(new NIOClient("127.0.0.1",8888)).start();
new Thread(new NIOClient("127.0.0.1",8888)).start();
}
}
客户端处理连接的线程
package com.learn.reactor.singlereactormodel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
/**
* autor:liman
* createtime:2019/10/15
* comment:
*/
@Slf4j
public class Connector implements Runnable{
private final Selector selector;
private final SocketChannel socketChannel;
Connector(SocketChannel socketChannel, Selector selector) {
this.socketChannel = socketChannel;
this.selector = selector;
}
@Override
public void run() {
try {
if (socketChannel.finishConnect()) { //这里连接完成(与服务端的三次握手完成)
log.info("已经完成{}的连接",socketChannel.getRemoteAddress());
new ClientHandler(socketChannel, selector); //连接建立完成后,接下来的动作交给Handler去处理(读写等)
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端真正处理业务的类:
package com.learn.reactor.singlereactormodel;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.atomic.AtomicInteger;
/**
* autor:liman
* createtime:2019/10/15
* comment:
*/
@Slf4j
public class ClientHandler implements Runnable {
private final SelectionKey selectionKey;
private final SocketChannel socketChannel;
private ByteBuffer readBuffer = ByteBuffer.allocate(2048);
private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
private final static int READ = 0;
private final static int SEND = 1;
private int status = SEND; //与服务端不同,默认最开始是发送数据
private AtomicInteger counter = new AtomicInteger();
ClientHandler(SocketChannel socketChannel, Selector selector) throws IOException, IOException {
this.socketChannel = socketChannel; //接收客户端连接
this.socketChannel.configureBlocking(false); //置为非阻塞模式(selector仅允非阻塞模式)
selectionKey = socketChannel.register(selector, 0); //将该客户端注册到selector,得到一个SelectionKey,以后的select到的就绪动作全都是由该对象进行封装
selectionKey.attach(this); //附加处理对象,当前是Handler对象,run是对象处理业务的方法
selectionKey.interestOps(SelectionKey.OP_WRITE); //走到这里,说明之前Connect已完成,那么接下来就是发送数据,因此这里首先将写事件标记为“感兴趣”事件
selector.wakeup(); //唤起select阻塞
}
@Override
public void run() {
try {
switch (status) {
case SEND:
send();
break;
case READ:
read();
break;
default:
}
} catch (IOException e) { //这里的异常处理是做了汇总,同样的,客户端也面临着正在与服务端进行写/读数据时,突然因为网络等原因,服务端直接断掉连接,这个时候客户端需要关闭自己并退出程序
System.err.println("send或read时发生异常!异常信息:" + e.getMessage());
selectionKey.cancel();
try {
socketChannel.close();
} catch (IOException e2) {
System.err.println("关闭通道时发生异常!异常信息:" + e2.getMessage());
e2.printStackTrace();
}
}
}
void send() throws IOException {
if (selectionKey.isValid()) {
sendBuffer.clear();
int count = counter.incrementAndGet();
if (count <= 10) {
String sendMessage = "客户端发送的第" + count + "条消息";
// sendBuffer = BufferUtils.writeByteBufferInfo("客户端发送的第" + count + "条消息");
// System.out.println("客户端发送的第["+count+"]条消息"+BufferUtils.readByteBufferInfo(sendBuffer));
sendBuffer.put(sendMessage.getBytes());
sendBuffer.flip(); //切换到读模式,用于让通道读到buffer里的数据
// String sendMessage=BufferUtils.readByteBufferInfo(sendBuffer);
// sendBuffer.reset();
// log.info("客户端发送的第{}条消息,消息内容为:{}",count,sendMessage);
socketChannel.write(sendBuffer);
// String sendMessage = BufferUtils.readByteBufferInfo(sendBuffer);
log.info("客户端发送的第{}条消息,消息内容为:{}", count, sendMessage);
//则再次切换到读,用以接收服务端的响应
status = READ;
selectionKey.interestOps(SelectionKey.OP_READ);
} else {
selectionKey.cancel();
socketChannel.close();
}
}
}
private void read() throws IOException {
if (selectionKey.isValid()) {
readBuffer.clear();
socketChannel.read(readBuffer);
log.info("收到来自客户端的消息,消息内容为:{}", BufferUtils.readByteBufferInfo(readBuffer));
// System.out.println("收到来自客户端的消息"+BufferUtils.readByteBufferInfo(readBuffer));
//收到服务端的响应后,再继续往服务端发送数据
status = SEND;
selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写事件
}
}
}
运行结果如下:
服务端输出
客户端输出:
但是单线程的reactor模式缺点也很明显,这里处理数据依旧是阻塞的,多线程并没有起到多大的作用。
总结
本篇博客只是针对单线程Reactor模式下的实例总结而已,这里所说的单Reactor指的是处理Selector连接建立事件的Reactor只有一个(或者可以简单理解为真正做事的Selector只有一个),同时后面的读和写其实也是单线程在处理,所以这里统称为单Reactor单线程的Reactor的模型
参考资料: