Reactor模型-單線程版

Reactor模型是典型的事件驅動模型。在網絡編程中,所謂的事件當然就是read、write、bind、connect、close等這些動作了。Reactor模型的實現有很多種,下面介紹最基本的三種:

  • 單線程版
  • 多線程版
  • 主從多線程版
Key Word:Java NIO,Reactor模型,Java并發編程,Event-Driven

單線程版本

結構圖(引用自Doug Lea的Scalable IO in Java)如下:

Reactor模型圖

上圖中Reactor是一個典型的事件驅動中心,客戶端發起請求并建立連接時,會觸發注冊在多路復用器Selector上的SelectionKey.OP_ACCEPT事件,綁定在該事件上的Acceptor對象的職責就是接受請求,為接下來的讀寫操作做準備。

Reactor設計如下:

/**
 * Reactor
 * 
 * @author wqx
 *
 */
public class Reactor implements Runnable {

    private static final Logger LOG = LoggerFactory.getLogger(Reactor.class);
    
    private Selector selector;
    
    private ServerSocketChannel ssc;

    private Handler DEFAULT_HANDLER = new Handler(){
        @Override
        public void processRequest(Processor processor, ByteBuffer msg) {
            //NOOP
        }
    };
    private Handler handler = DEFAULT_HANDLER;
    
    
    /**
     * 啟動階段
     * @param port
     * @throws IOException
     */
    public Reactor(int port, int maxClients, Handler serverHandler) throws IOException{
        selector = Selector.open();
        ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.socket().bind(new InetSocketAddress(port));
        
        this.handler = serverHandler;
        SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
        sk.attach(new Acceptor());
    }
    /**
     * 輪詢階段
     */
    @Override
    public void run() {
        while(!ssc.socket().isClosed()){
            try {
                selector.select(1000);
                Set<SelectionKey> keys;
                synchronized(this){
                    keys = selector.selectedKeys();
                }
                Iterator<SelectionKey> it = keys.iterator();
                while(it.hasNext()){
                    SelectionKey key = it.next();
                    dispatch(key);
                    it.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        close();
    }
    
    public void dispatch(SelectionKey key){
        Runnable r = (Runnable)key.attachment();
        if(r != null)
            r.run();
    }
    /**
     * 用于接受TCP連接的Acceptor
     * 
     */
    class Acceptor implements Runnable{

        @Override
        public void run() {
            SocketChannel sc;
            try {
                sc = ssc.accept();
                if(sc != null){
                    new Processor(Reactor.this,selector,sc);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    public void close(){
        try {
            selector.close();
            if(LOG.isDebugEnabled()){
                LOG.debug("Close selector");
            }
        } catch (IOException e) {
            LOG.warn("Ignoring exception during close selector, e=" + e);
        }
    }
    public void processRequest(Processor processor, ByteBuffer msg){
        if(handler != DEFAULT_HANDLER){
            handler.processRequest(processor, msg);
        }
    }
}

上面是典型的單線程版本的Reactor實現,實例化Reactor對象的過程中,在當前多路復用器Selector上注冊了OP_ACCEPT事件,當OP_ACCEPT事件發生后,Reactor通過dispatch方法執行Acceptor的run方法,Acceptor類的主要功能就是接受請求,建立連接,并將代表連接建立的SocketChannel以參數的形式構造Processor對象。

Processor的任務就是進行I/O操作。

下面是Processor的源碼:

/**
 * Server Processor
 * 
 * @author wqx
 */
public class Processor implements Runnable {

    private static final Logger LOG = LoggerFactory.getLogger(Processor.class);

    Reactor reactor;

    private SocketChannel sc;

    private final SelectionKey sk;

    private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);

    private ByteBuffer inputBuffer = lenBuffer;

    private ByteBuffer outputDirectBuffer = ByteBuffer.allocateDirect(1024 * 64);

    private LinkedBlockingQueue<ByteBuffer> outputQueue = new LinkedBlockingQueue<ByteBuffer>();

    public Processor(Reactor reactor, Selector sel,SocketChannel channel) throws IOException{
        this.reactor = reactor;
        sc = channel;
        sc.configureBlocking(false);
        sk = sc.register(sel, SelectionKey.OP_READ);
        sk.attach(this);
        sel.wakeup();
    }

    @Override
    public void run() {
        if(sc.isOpen() && sk.isValid()){
            if(sk.isReadable()){
                doRead();
            }else if(sk.isWritable()){
                doSend();
            }
        }else{
            LOG.error("try to do read/write operation on null socket");
            try {
                if(sc != null)
                    sc.close();
            } catch (IOException e) {}
        }
    }
    private void doRead(){
        try {
            int byteSize = sc.read(inputBuffer);
            
            if(byteSize < 0){
                LOG.error("Unable to read additional data");
            }
            if(!inputBuffer.hasRemaining()){
                
                if(inputBuffer == lenBuffer){
                    //read length
                    inputBuffer.flip();
                    int len = inputBuffer.getInt();
                    if(len < 0){
                        throw new IllegalArgumentException("Illegal data length");
                    }
                    //prepare for receiving data
                    inputBuffer = ByteBuffer.allocate(len);
                }else{
                    //read data
                    if(inputBuffer.hasRemaining()){
                        sc.read(inputBuffer);
                    }
                    if(!inputBuffer.hasRemaining()){
                        inputBuffer.flip();
                        processRequest();
                        //clear lenBuffer and waiting for next reading operation 
                        lenBuffer.clear();
                        inputBuffer = lenBuffer;
                    }
                }
            }
        } catch (IOException e) {
            LOG.error("Unexcepted Exception during read. e=" + e);
            try {
                if(sc != null)
                    sc.close();
            } catch (IOException e1) {
                LOG.warn("Ignoring exception when close socketChannel");
            }
        }
    }

    /**
     * process request and get response
     * 
     * @param request
     * @return
     */
    private void processRequest(){
        reactor.processRequest(this,inputBuffer);
    }
    private void doSend(){
        try{
            /**
             * write data to channel:
             * step 1: write the length of data(occupy 4 byte)
             * step 2: data content
             */
            if(outputQueue.size() > 0){
                ByteBuffer directBuffer = outputDirectBuffer;
                directBuffer.clear();
                
                for(ByteBuffer buf : outputQueue){
                    buf.flip();
                    
                    if(buf.remaining() > directBuffer.remaining()){
                        //prevent BufferOverflowException
                        buf = (ByteBuffer) buf.slice().limit(directBuffer.remaining());
                    }
                    //transfers the bytes remaining in buf into  directBuffer
                    int p = buf.position();
                    directBuffer.put(buf);
                    //reset position
                    buf.position(p);

                    if(!directBuffer.hasRemaining()){
                        break;
                    }
                }
                directBuffer.flip();
                int sendSize = sc.write(directBuffer);
                
                while(!outputQueue.isEmpty()){
                    ByteBuffer buf = outputQueue.peek();
                    int left = buf.remaining() - sendSize;
                    if(left > 0){
                        buf.position(buf.position() + sendSize);
                        break;
                    }
                    sendSize -= buf.remaining();
                    outputQueue.remove();
                }
            }
            synchronized(reactor){
                if(outputQueue.size() == 0){
                    //disable write
                    disableWrite();
                }else{
                    //enable write
                    enableWrite();
                }
            }
        } catch (CancelledKeyException e) {
            LOG.warn("CancelledKeyException occur e=" + e);
        } catch (IOException e) {
            LOG.warn("Exception causing close, due to " + e);
        }
    }
    public void sendBuffer(ByteBuffer bb){
        try{
            synchronized(this.reactor){
                if(LOG.isDebugEnabled()){
                    LOG.debug("add sendable bytebuffer into outputQueue");
                }
                //wrap ByteBuffer with length header
                ByteBuffer wrapped = wrap(bb);
                
                outputQueue.add(wrapped);
                
                enableWrite();
            }
        }catch(Exception e){
            LOG.error("Unexcepted Exception: ", e);
        }
    }
    
    private ByteBuffer wrap(ByteBuffer bb){
        bb.flip();
        lenBuffer.clear();
        int len = bb.remaining();
        lenBuffer.putInt(len);
        ByteBuffer resp = ByteBuffer.allocate(len+4);
        lenBuffer.flip();
        
        resp.put(lenBuffer);
        resp.put(bb);
        return resp;
    }
    private void enableWrite(){
        int i = sk.interestOps();
        if((i & SelectionKey.OP_WRITE) == 0){
            sk.interestOps(i | SelectionKey.OP_WRITE);
        }
    }
    private void disableWrite(){
        int i = sk.interestOps();
        if((i & SelectionKey.OP_WRITE) == 1){
            sk.interestOps(i & (~SelectionKey.OP_WRITE));           
        }
    }
}

其實Processor要做的事情很簡單,就是向selector注冊感興趣的讀寫時間,OP_READ或OP_WRITE,然后等待事件觸發,做相應的操作。

    @Override
    public void run() {
        if(sc.isOpen() && sk.isValid()){
            if(sk.isReadable()){
                doRead();
            }else if(sk.isWritable()){
                doSend();
            }
        }else{
            LOG.error("try to do read/write operation on null socket");
            try {
                if(sc != null)
                    sc.close();
            } catch (IOException e) {}
        }
    }

而doRead()和doSend()方法稍微復雜了一點,這里其實處理了用TCP協議進行通信時必須要解決的問題:TCP粘包拆包問題

TCP粘包拆包問題

我們都知道TCP協議是面向字節流的,而字節流是連續的,無法有效識別應用層數據的邊界。如下圖:


粘包拆包示意圖

上圖顯示的應用層有三個數據包,D1,D2,D3.當應用層數據傳到傳輸層后,可能會出現粘包拆包現象。

TCP協議的基本傳輸單位是報文段,而每個報文段最大有效載荷是有限制的,一般以太網MTU為1500,去除IP頭20B,TCP頭20B,那么剩下的1460B就是傳輸層最大報文段的有效載荷。如果應用層數據大于該值(如上圖中的數據塊D2),那么傳輸層就會進行拆分重組。

解決方案

  1. 消息定長(通信雙方發送的消息固定長度,缺點很明顯:浪費可恥?。。。?/li>
  2. 每個消息之間加分割符(缺點:消息編解碼耗時,并且如果消息體中本省就包含分隔字符,需要進行轉義,效率低)
  3. 每個數據包加個Header?。。。╤eader中指定后面數據的長度,這不就是Tcp、Ip協議通用的做法么。。。哈哈)

采用方案三

示意圖如下:


數據包結構

header區占用4B,內容為數據的長度。too simple。。。-_-

理論有了,下面具體分析下Read、Write的實現過程:

doRead
inputBuffer負責接受數據,lenBuffer負責接受數據長度,初始化的時候,將lenBuffer賦給inputBuffer,定義如下:

private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
private ByteBuffer inputBuffer = lenBuffer;
  1. 如果inputBuffer == lenBuffer,那么從inputBuffer中讀取出一個整型值len,這個值就是接下來要接受的數據的長度。同時分配一個大小為len的內存空間,并復制給inputBuffer,準備接受數據!??!
    private void doRead(){
        try {
            int byteSize = sc.read(inputBuffer);
            
            if(byteSize < 0){
                LOG.error("Unable to read additional data");
            }
            if(!inputBuffer.hasRemaining()){
                
                if(inputBuffer == lenBuffer){
                    //read length
                    inputBuffer.flip();
                    int len = inputBuffer.getInt();
                    if(len < 0){
                        throw new IllegalArgumentException("Illegal data length");
                    }
                    //prepare for receiving data
                    inputBuffer = ByteBuffer.allocate(len);
                else{...}
  1. 如果inputBuffer != lenBuffer,那么開始接受數據吧!
if(inputBuffer == lenBuffer){
        //。。。
}else{
    //read data
    if(inputBuffer.hasRemaining()){
        sc.read(inputBuffer);
    }
    if(!inputBuffer.hasRemaining()){
        inputBuffer.flip();
        processRequest();
        //clear lenBuffer and waiting for next reading operation 
        lenBuffer.clear();
        inputBuffer = lenBuffer;
    }
}

注意

  1. 必須保證緩沖區是滿的,即inputBuffer.hasRemaining()=false
  2. processRequest后,將inputBuffer重新賦值為lenBuffer,為下一次讀操作做準備。

doWrite

用戶調用sendBuffer方法發送數據,其實就是將數據加入outputQueue,這個outputQueue就是一個發送緩沖隊列。

public void sendBuffer(ByteBuffer bb){
        try{
            synchronized(this.reactor){
                if(LOG.isDebugEnabled()){
                    LOG.debug("add sendable bytebuffer into outputQueue");
                }
                //wrap ByteBuffer with length header
                ByteBuffer wrapped = wrap(bb);
                
                outputQueue.add(wrapped);
                
                enableWrite();
            }
        }catch(Exception e){
            LOG.error("Unexcepted Exception: ", e);
        }
    }

doSend方法就很好理解了,無非就是不斷從outputQueue中取數據,然后寫入channel中即可。過程如下:

將發送隊列outputQueue中的數據寫入緩沖區outputDirectBuffer:

  1. 清空outputDirectBuffer,為發送數據做準備
  2. 將outputQueue數據寫入outputDirectBuffer
  3. 調用socketChannel.write(outputDirectBuffer);將outputDirectBuffer寫入socket緩沖區

執行步驟2的時候,我們可能會遇到這么幾種情況:

1.某個數據包大小超過了outputDirectBuffer剩余空間大小

2.outputDirectBuffer已被填滿,但是outputQueue仍有待發送的數據

執行步驟3的時候,也可能出現下面兩種情況:

1.outputDirectBuffer被全部寫入socket緩沖區

2.outputDirectBuffer只有部分數據或者壓根就沒有數據被寫入socket緩沖區

實現過程可以結合源碼,這里重點分析下面幾個點:

為什么需要重置buf的position

int p = buf.position();
directBuffer.put(buf);
//reset position
buf.position(p);

寫入directBuffer的數據是即將被寫入SocketChannel的數據,問題就在于:當我們調用

int sendSize = sc.write(directBuffer);

的時候,directBuffer中的數據都被寫入Channel了嗎?明顯是不確定的(具體可以看java.nio.channels.SocketChannel.write(ByteBuffer src)的doc文檔)

上面的問題如何解決

思路很簡單,根據write方法返回值sendSize,遍歷outputQueue中的ByteBuffer,根據buf.remaining()和sendSize的大小,才可以確定buf是否真的被發送了。如下所示:

while(!outputQueue.isEmpty()){
    ByteBuffer buf = outputQueue.peek();
    int left = buf.remaining() - sendSize;
    if(left > 0){
        buf.position(buf.position() + sendSize);
        break;
    }
    sendSize -= buf.remaining();
    outputQueue.remove();
}

網絡通信基本解決,上面的處理思路是參照Zookeeper網絡模塊的實現,有興趣可以看Zookeeper相應源碼。

測試

Server端:

public class ServerTest {

    private static int PORT = 8888;
    
    public static void main(String[] args) throws IOException, InterruptedException {
        
        Thread t = new Thread(new Reactor(PORT,1024,new MyHandler()));
        t.start();
        System.out.println("server start");
        t.join();
    }
}

用戶自定義Handler:

public class MyHandler implements Handler {
    
    @Override
    public void processRequest(Processor processor, ByteBuffer msg) {
        byte[] con = new byte[msg.remaining()];
        msg.get(con);
        
        String str = new String(con,0,con.length);
        
        String resp = "";
        switch(str){
        case "request1":resp = "response1";break;
        case "request2":resp = "response2";break;
        case "request3":resp = "response3";break;
        default :resp = "";
        }
        
        ByteBuffer buf = ByteBuffer.allocate(resp.getBytes().length);
        buf.put(resp.getBytes());
        
        processor.sendBuffer(buf);
    }
}

client端

public class ClientTest {

    private static String HOST = "localhost";
    private static int PORT = 8888;

    public static void main(String[] args) throws IOException {
        
        Client client = new Client();
        client.socket().setTcpNoDelay(true);
        
        client.connect(
                new InetSocketAddress(HOST,PORT));
        
        ByteBuffer msg;
        for(int i = 1; i <= 3; i++){
            msg = ByteBuffer.wrap(("request" + i).getBytes());
            System.out.println("send-" + "request" + i);
            
            ByteBuffer resp = client.send(msg);
            byte[] retVal = new byte[resp.remaining()];
            resp.get(retVal);

            System.out.println("receive-" + new String(retVal,0,retVal.length));
            
        }
    }
}

輸出:

send-request1
receive-response1
send-request2
receive-response2
send-request3
receive-response3

Client是一個客戶端工具類,簡單封裝了發送ByteBuffer前,添加header的邏輯。詳見源碼。Client.java

總結

在這種實現方式中,dispatch方法是同步阻塞的?。?!所有的IO操作和業務邏輯處理都在NIO線程(即Reactor線程)中完成。如果業務處理很快,那么這種實現方式沒什么問題,不用切換到用戶線程。但是,想象一下如果業務處理很耗時(涉及很多數據庫操作、磁盤操作等),那么這種情況下Reactor將被阻塞,這肯定是我們不希望看到的。解決方法很簡單,業務邏輯進行異步處理,即交給用戶線程處理。

下面分析下單線程版的Reactor模型的缺點:

  • 自始自終都只有一個Reactor線程,缺點很明顯:Reactor意外掛了,整個系統也就無法正常工作,可靠性太差。
  • 單線程的另外一個問題是在大負載的情況下,Reactor的處理速度必然會成為系統性能的瓶頸。

如何解決上述問題呢?下文詳解Reactor多線程版本

GitHub完整源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容