針對上一篇博文,通過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。