在BIO时代,我们写一个服务端,由于其同步阻塞特性,我们采取的方式是这样的:
当服务端收到一个客户端请求后,不得不为这个客户端单独开一个线程来阻塞读取这个客户端后续发来的数据。如果学过多线程,我们很清楚,如果线程开多了是一件很糟糕的事情,过多的上线文切换,很耽误服务器的正常的工作。这也意味着这种模式的请求量瓶颈很低。故而在NIO应运而生,在NIO下服务端是这样的:
这种模式下,不需要为每个客户端连接单独开辟一个线程了,由于非阻塞的特性,我们只需要一个selector来轮训查看这些channel中资源获取是否完成,完成后再交给执行端。我们在听说NIO的时候,听过最多的一个词叫多路复用技术,那到底什么是多路复用呢,其实就是selector的阻塞复用,在BIO中每一个线程都需要阻塞等待资源的获取,而NIO中只需要一个selector进行阻塞,相当于每一个客户端都复用了这一个selector。而本文标题的Reactor线程模型,就是基于上面NIO这种,利用多路复用和事件驱动的线程模型。Reactor模型分为单reactor单线程模型、单reactor多线程模型,多reactor多线程模型。
里面两个核心名词,reactor,线程。reactor是什么,就是selector这种对象,他相当于一个事件驱动器,你可以在selector中注册一个事件,然后执行selector.select()方法,然后selector会陷入阻塞,如果当被注册的事件被触发时,他会结束阻塞。而线程即用来处理工作的。单reactor单线程即表示,所有的事件都注册到同一个reactor中,然后由一个线程来处理所有任务。那会有哪些事件呢?比如在服务端中,accept接收到客户端的链接请求是一个时间,读取到客户端的数据也是一个事件,我们把这些时间都注册到selector1中,然后当selector1监听到事件时,停止阻塞,都交给线程thread1来处理,这就是一个典型的单reactor单线程模型。
而当业务逻辑较复杂的,如果只有一个线程来处理效率显然会比较低下,因为后一个用户必须等待前一个用户的业务逻辑处理完才能继续,所有就有了单reactor多线程模型,多线程即利用一个线程池来处理请求,从而提高响应效率。但是当有海量链接进来是,一个reactor可能也无法支撑了,所有就有了多reactor多线程模型,让一个selector专门管理客户端的请求事件,另一个seletor来专门负责数据读取的事件。下面是一段单reactor单线程的代码,仅供参考:
package com.huifu.test.nio.reactor1;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
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;
/**
* 结合selector实现非阻塞服务器(单线程reactor模型)
*/
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
// serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//两种注册写法,下面这一种是先拿到一个没有对任何时间感兴趣的key对象,再给这个对象插入一个感兴趣的事件
SelectionKey selectionKey = serverSocketChannel.register(selector,0);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
//绑定端口代表服务启动成功了
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
System.out.println("启动成功");
while (true){
//运行selector,然后会进入阻塞,直到有事件通知才会返回,所以需要while循环,不断的让selector去阻塞等待
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterable = selectionKeys.iterator();
while(iterable.hasNext()){
SelectionKey key = iterable.next();
//为什么需要remove,原因是nio没有帮我们维护这个selectionKeys集合,每次返回的都是原来的那个集合,所以我们需要手动把处理过的对象从集合中移除掉
iterable.remove();
if(key.isAcceptable()){
SocketChannel socketChannel = ((ServerSocketChannel) key.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(3);
StringBuilder message = new StringBuilder();
while (true){
if(socketChannel.isOpen()){
int bufferLength = socketChannel.read(byteBuffer);
System.out.println("bufferLength="+bufferLength);
if(bufferLength==-1 || bufferLength==0){
break;
}else{
byteBuffer.flip();
byte[] messageByte = new byte[byteBuffer.remaining()];
byteBuffer.get(messageByte);
byteBuffer.clear();
message.append(new String(messageByte));
System.out.println("接收到客户端信息:"+message.toString());
}
}else{
break;
}
}
System.out.println("最终接收到客户端信息:"+message.toString());
ByteBuffer byteBuffer1 = ByteBuffer.wrap(("服务端接受到了你的消息:"+message.toString()).getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer1);
}
}
}
}
}
我们常用的网络通讯框架Netty,他的核心设计其实就是多reactor多线程模型,之前考虑过一个问题就是Netty为什么是基于NIO设计的,因为前一张讲过BIO,NIO,AIO的区别,为什么作为一个优秀的网络通讯框架他是基于NIO而不是AIO的,原因是Linux中对于AIO的实现和NIO的底层实现其实是一样的都是基于EPOLL的(这是什么自行学习),性能上并没有什么提升,而Windows虽然做了AIO的改进,但是Netty是为服务端设计的,服务端很少用到windows。