Reactor模型-單線程版

Reactor模型是典型的事件驅(qū)動模型。在網(wǎng)絡(luò)編程中,所謂的事件當(dāng)然就是read、write、bind、connect、close等這些動作了。Reactor模型的實現(xiàn)有很多種,下面介紹最基本的三種:

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

單線程版本

結(jié)構(gòu)圖(引用自Doug Lea的Scalable IO in Java)如下:

Reactor模型圖

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

Reactor設(shè)計如下:

/**
 * 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實現(xiàn),實例化Reactor對象的過程中,在當(dāng)前多路復(fù)用器Selector上注冊了OP_ACCEPT事件,當(dāng)OP_ACCEPT事件發(fā)生后,Reactor通過dispatch方法執(zhí)行Acceptor的run方法,Acceptor類的主要功能就是接受請求,建立連接,并將代表連接建立的SocketChannel以參數(shù)的形式構(gòu)造Processor對象。

Processor的任務(wù)就是進行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,然后等待事件觸發(fā),做相應(yīng)的操作。

    @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()方法稍微復(fù)雜了一點,這里其實處理了用TCP協(xié)議進行通信時必須要解決的問題:TCP粘包拆包問題

TCP粘包拆包問題

我們都知道TCP協(xié)議是面向字節(jié)流的,而字節(jié)流是連續(xù)的,無法有效識別應(yīng)用層數(shù)據(jù)的邊界。如下圖:


粘包拆包示意圖

上圖顯示的應(yīng)用層有三個數(shù)據(jù)包,D1,D2,D3.當(dāng)應(yīng)用層數(shù)據(jù)傳到傳輸層后,可能會出現(xiàn)粘包拆包現(xiàn)象。

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

解決方案

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

采用方案三

示意圖如下:


數(shù)據(jù)包結(jié)構(gòu)

header區(qū)占用4B,內(nèi)容為數(shù)據(jù)的長度。too simple。。。-_-

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

doRead
inputBuffer負(fù)責(zé)接受數(shù)據(jù),lenBuffer負(fù)責(zé)接受數(shù)據(jù)長度,初始化的時候,將lenBuffer賦給inputBuffer,定義如下:

private final ByteBuffer lenBuffer = ByteBuffer.allocate(4);
private ByteBuffer inputBuffer = lenBuffer;
  1. 如果inputBuffer == lenBuffer,那么從inputBuffer中讀取出一個整型值len,這個值就是接下來要接受的數(shù)據(jù)的長度。同時分配一個大小為len的內(nèi)存空間,并復(fù)制給inputBuffer,準(zhǔn)備接受數(shù)據(jù)?。?!
    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,那么開始接受數(shù)據(jù)吧!
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. 必須保證緩沖區(qū)是滿的,即inputBuffer.hasRemaining()=false
  2. processRequest后,將inputBuffer重新賦值為lenBuffer,為下一次讀操作做準(zhǔn)備。

doWrite

用戶調(diào)用sendBuffer方法發(fā)送數(shù)據(jù),其實就是將數(shù)據(jù)加入outputQueue,這個outputQueue就是一個發(fā)送緩沖隊列。

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中取數(shù)據(jù),然后寫入channel中即可。過程如下:

將發(fā)送隊列outputQueue中的數(shù)據(jù)寫入緩沖區(qū)outputDirectBuffer:

  1. 清空outputDirectBuffer,為發(fā)送數(shù)據(jù)做準(zhǔn)備
  2. 將outputQueue數(shù)據(jù)寫入outputDirectBuffer
  3. 調(diào)用socketChannel.write(outputDirectBuffer);將outputDirectBuffer寫入socket緩沖區(qū)

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

1.某個數(shù)據(jù)包大小超過了outputDirectBuffer剩余空間大小

2.outputDirectBuffer已被填滿,但是outputQueue仍有待發(fā)送的數(shù)據(jù)

執(zhí)行步驟3的時候,也可能出現(xiàn)下面兩種情況:

1.outputDirectBuffer被全部寫入socket緩沖區(qū)

2.outputDirectBuffer只有部分?jǐn)?shù)據(jù)或者壓根就沒有數(shù)據(jù)被寫入socket緩沖區(qū)

實現(xiàn)過程可以結(jié)合源碼,這里重點分析下面幾個點:

為什么需要重置buf的position

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

寫入directBuffer的數(shù)據(jù)是即將被寫入SocketChannel的數(shù)據(jù),問題就在于:當(dāng)我們調(diào)用

int sendSize = sc.write(directBuffer);

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

上面的問題如何解決

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

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();
}

網(wǎng)絡(luò)通信基本解決,上面的處理思路是參照Zookeeper網(wǎng)絡(luò)模塊的實現(xiàn),有興趣可以看Zookeeper相應(yīng)源碼。

測試

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是一個客戶端工具類,簡單封裝了發(fā)送ByteBuffer前,添加header的邏輯。詳見源碼。Client.java

總結(jié)

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

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

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

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

GitHub完整源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容