针对上一篇博文,通过selector类确实是可以达到通过一个线程管理多个客户端连接,类似消息监听与多路复用的作用,但是依然是存在性能问题的,为什么这么说呢?比如A客户端发送请求过来服务端接受到了,然后响应请求,但是如果是一些比较耗时的业务操作,那么服务端就一直只能在处理完业务操作后才能处理处理其它客户端的请求,也就是会造成堵车现象,这是性能不足点1。(可以采用线程池+CompletedFuture来达到异步非阻效果)。
于是便有了如下的Reactor线程模型了,这也是Doug Lea提出的,这个也与selector设计(单个Reactor线程模型)相吻合。
但与此同时,但当海量连接到服务器的话,该线程既要去接受客户端的连接工作,又要去接收客户端的请求,然后还要响应(当然这里响应所需的数据,我们已经用线程池解决了,解决了堵车问题),但很显然,光一个线程去处理这些客户端的连接、接收客户端的请求、响应是明显不够的,这时候也可以用线程池或者线程组的方式去解决,这也是不足点2。(将客户端的连接工作放在一个单独的线程,客户端连接完成后,在把客户端交的连接给另一个线程去处理IO操作,也就是下图的subReactor线程池/组,而业务耗时操作交给线程池去处理),所以要对该服务端在优化,于是又有下面的多Reactor线程模型。
下面是我用ppt画的,描述基于服务端如何根据此模型设计一个比较好的类图架构。大概的意思就是当服务端收到海量的连接时,我这里会做类似的生活场景比喻,便于自己理解和记忆。
- 首先通过mainReactor类去做一个客户端的连接分配,比如将服务端注册到select、绑定端口、开启select管家,当有连接进来则分配给Acceptor类,相当于它是一个的入口,可以比作是按摩店的一个门卫,给你指路,引导人流正确进入按摩店,当你进店后,也就是连接成功后,他就会给你带到大堂经理那边去。
- Acdeptor类呢则是那位大堂经理,它则会给你分配服务前台小姐姐,它会带你找你的技师,但是前台小姐姐毕竟有限,她是一对多的操作,她不可能只带你一个人去见技师,可能是带着一群人见技师(所以她是一个线程处理多个客户端的连接,并且同一个线程用同一个select),如果这时候你沉默不语(客户端没发数据过来)她是不会给你分配技师的,你剪个头发都会问你找几号理发师对吧,是托尼还是....,一旦你们一群人中有人说话了(发数据给服务端),那么小姐姐就会监听到你的意愿,给你分配一个handler类
- Hanler则是你朝思暮想的技师了,它会得到你的诉求,处理业务逻辑,然后响应给你,至此流程结束。
很显然上述问题得以解决,现在对上述进行一个代码实现,为了让代码可读性更高,我会将mainReactor线程(客户端的连接)和subeactor线程()抽象出来封装在一个抽象类里并且继承Thread,里面写一个抽象方法handler(),而handler()方法就是去处理两个逻辑,一个是mainReactor干的事情也就是客户端的连接,另一个是subReactor干的事情也就是接受和响应客户端数据,继承Thread的好处是在实现run方法里面调用handler,当线程启动时就需要去调用handler去实现各自要处理的逻辑。
代码实现过程:
抽象类:ReactorThread(将监听事件单独抽象出来,一旦有事件监听则调用handler方法)
package com.dongnaoedu.network.humm.ReactorPkg;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Set;
/**
* @author Heian
* @time 19/06/30 15:20
* @description:作为服务端的事件监听器,监听客户端的连接和客户端的发送的数据以及及时响应
*/
abstract class ReactorThread extends Thread {
volatile boolean running = false;
Selector selector;
//Selector监听到有事件后,调用这个方法
public abstract void hanler(SelectableChannel channel);
public ReactorThread() throws IOException {
selector = Selector.open ();
System.out.println (selector.toString ());
}
/**
* 服务端的注册
* 注册两次需要用到:服务端启动注册到select和接受接受客户端连接注册accept事件
* @param :ops=0表示注册的事件可以自定义 attachment:channel因为
*/
public SelectionKey register(SelectableChannel channel) throws ClosedChannelException {
return channel.register (selector,0,channel);
}
//每个线程启动默认是false,启动后为true,便不会再次启动
public void singleStart() {
if (!running){//防止线程轮询超过一轮多次启动
running = true;
start ();
}
}
@Override
public void run() {
//当此线程启动的时候,说明就有业务要处理了,此线程可能作为subReactor线程一样监听多个客户端的连接
while (running){
try {
selector.select ();//超过1s无返回值,变打断阻塞
Set<SelectionKey> keys = selector.selectedKeys ();
Iterator<SelectionKey> it = keys.iterator ();
while (it.hasNext ()){
SelectionKey key = it.next ();
it.remove ();
int ops = key.readyOps ();
if ( (ops & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT )) != 0 ){
//这时候注册的可能是连接连接事件则是则是socketchannel 如果是服务端启动注册的则是serverSocketchannel
SelectableChannel channel = (SelectableChannel)key.attachment ();
//SelectableChannel channel1 = key.channel ();也可以不通过附件拿,这样register方法第三个参数也可以不传
channel.configureBlocking (false);
//连接进来后则要去处理对应的业务逻辑(mainReactor 和 subReactor)
try {
hanler (channel);
} catch (Exception e) {
e.printStackTrace ();
}
if (!channel.isOpen ())
key.channel ();/// 如果关闭了,就取消这个KEY的订阅
}
}
} catch (IOException e) {
e.printStackTrace ();
}
}
}
}
*服务端代码:
package com.dongnaoedu.network.humm.ReactorPkg;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author Heian
* @time 19/06/30 16:29
* @description:基于多Reactor线程模型的服务器
*/
public class NioServer {
private ServerSocketChannel serverSocketChannel;
// 1、accept处理reactor线程 (accept线程)
private ReactorThread[] mainReactorThreads = new ReactorThread[1];
// 2、io处理reactor线程 (I/O线程)
private ReactorThread[] subReactorThreads = new ReactorThread[8];//性能不足点二:解决办法,创建创建多个线程来处理io,并且单个线程管理多个客户端的连接
// 3、处理业务操作的线程
private static ExecutorService workPool = Executors.newCachedThreadPool();//性能不足点一:解决办法,创建业务线程池
/**
* 初始化线程组:给mainReactorThread线程组分配数量为1个 ReactorThread线程数组(也可以多个)
* 给subReactorThread线程组分配线程数量为8个ReactorThread线程数组
* 二者统称为Reactor抽象类,主要的作用就是监听事件:1个是处理客户端的连接,另一个是接受客户端发出的数据,并处理;
*/
public void initMainAndSUbReactor() throws IOException {
// 创建mainReactor线程, 只负责处理serverSocketChannel
for(int i=0;i<mainReactorThreads.length;i++){
AtomicInteger atomicInteger = new AtomicInteger (0);
//通过启动main线程通过唤醒机制去唤醒sub线程
mainReactorThreads[i] = new ReactorThread () {
@Override
public void hanler(SelectableChannel channel) {
//当客户端连接进来后,分发给I/O线程继续去读取数据
try {
ServerSocketChannel ServerSocketChannel= (ServerSocketChannel) channel;
SocketChannel socketChannel = ServerSocketChannel.accept ();
System.out.println (Thread.currentThread ().getName () + "收到客户端连接,男朋友为:" + socketChannel.toString () );
socketChannel.configureBlocking (false);//客户端通道也设置为非阻塞模式
int index = atomicInteger.getAndIncrement () % subReactorThreads.length;
subReactorThreads[index].singleStart ();
//启动一个main线程意味着,有客户端连接进来,并且告诉subReactor时刻做好准备等待客户端发来数据并返回
SelectionKey socketKey = subReactorThreads[index].register (socketChannel);
socketKey.interestOps (SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace ();
}
}
};
}
//创建subReactor线程,只负责接收客户端数据,和响应请求
for (int i=0;i<subReactorThreads.length;i++){
subReactorThreads[i] = new ReactorThread () {
@Override
public void hanler(SelectableChannel channel) {
try {
SocketChannel ch = (SocketChannel) channel;
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (ch.isOpen() && ch.read(requestBuffer) != -1) {
// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
if (requestBuffer.position() > 0) break;
}
if (requestBuffer.position() == 0) return; // 如果没数据了, 则不继续后面的处理
requestBuffer.flip();//记得切换至读模式才能写数据
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(Thread.currentThread().getName() + "收到数据,来自:" + ch.getRemoteAddress() + new String(content));
// TODO 业务操作 数据库、接口...
workPool.submit(() -> {
try {
TimeUnit.SECONDS.sleep (1);
System.out.println ("selectUserById接口请求完成");
} catch (InterruptedException e) {
e.printStackTrace ();
}
});
// 接口返回结果 200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
ch.write(buffer);
}
}catch (Exception e){
e.printStackTrace ();
}
}
};
}
}
// 初始化服务端的channel,并且开启mainReactor线程
public void regAndDisServerSocket(int port) throws IOException {
serverSocketChannel = ServerSocketChannel.open ();
serverSocketChannel.configureBlocking (false);
//随机分配mainReactor,并启该线程
int index = new Random ().nextInt (mainReactorThreads.length);
mainReactorThreads[index].singleStart ();
SelectionKey selectionKey = mainReactorThreads[index].register (serverSocketChannel);
selectionKey.interestOps (SelectionKey.OP_ACCEPT);//设置兴趣事件
ServerSocket socket = serverSocketChannel.socket ();
socket.bind (new InetSocketAddress (port));
System.out.println ("服务器启动");
}
public static void main(String[] args) throws Exception{
NioServer nioServer = new NioServer ();
//初始化mainReactor 和 subReactor线程组
nioServer.initMainAndSUbReactor();
//有了两个线程组,两个线程组也都知道自己该干什么事情了,所以需要启动服务和启动mainReactor线程去调用subReactor线程
nioServer.regAndDisServerSocket (8080);
}
}
然后随便启动两个客户端,或者直接直接打开两个浏览器窗口地址输入:localhost:8080,发送数据,然后如图,得以成功。
至此优化完成,其实这里比较难的就是这种设计思路和梳理这里面的关系,方能写出这些代码。一是要知道是通过主线程启动去启动mainReacyorThreads然后再通过这个线程的启动去唤醒subReactorThreads,二是每次启动一个线程无论是mainReacyorThreads还是subReactorThreads都会通过构造方法产生一个新的selector,来达到多路监听。理解这点看懂这些代码基本上没啥大问题,另外推荐一个比较好看类之间的继承关系的类图,这是idea自带的,比较好用。比如我这段代码:
/**
* 服务端的注册
* 注册两次需要用到:服务端启动注册到select和接收客户端连接注册accept事件
* @param :ops=0表示注册的事件可以自定义 attachment:channel因为
*/
public SelectionKey register(SelectableChannel channel) throws ClosedChannelException {
return channel.register (selector,0,channel);
}
为什么参数放SelectableChannel这个类,就像我解释的那样,因为我启动服务端注册select需要用到ServerSocketChannel,接受客户端连接我又要socketChannel,而他们之间的类图如下:
利用多态当然可以选择SelectableChannel,当然选择AbstractSelectChannel,今天的学习就此告一段落,下一篇博文,rpc框架与netty。