Getty – Java NIO 框架設計與實現

肖漢鬆發表於2016-08-03

前言

Getty是我為了學習 Java NIO 所寫的一個 NIO 框架,實現過程中參考了 Netty 的設計,同時使用 Groovy 來實現。雖然只是玩具,但是麻雀雖小,五臟俱全,在實現過程中,不僅熟悉了 NIO 的使用,還借鑑了很多 Netty 的設計思想,提升了自己的編碼和設計能力。

至於為什麼用 Groovy 來寫,因為我剛學了 Groovy,正好拿來練手,加上 Groovy 是相容 Java 的,所以只是語法上的差別,底層實現還是基於 Java API的。

Getty 的核心程式碼行數不超過 500 行,一方面得益於 Groovy 簡潔的語法,另一方面是因為我只實現了核心的邏輯,最複雜的其實是解碼器實現。腳手架容易搭,摩天大樓哪有那麼容易蓋,但用來學習 NIO 足以。

執行緒模型

Getty 使用的是 Reactor 多執行緒模型

reactor

  1. 有專門一個 NIO 執行緒- Acceptor 執行緒用於監聽服務端,接收客戶端的 TCP 連線請求,然後將連線分配給工作執行緒,由工作執行緒來監聽讀寫事件。
  2. 網路 IO 操作-讀/寫等由多個工作執行緒負責,由這些工作執行緒負責訊息的讀取、解碼、編碼和傳送。
  3. 1 個工作執行緒可以同時處理N條鏈路,但是 1 個鏈路只對應 1 個工作執行緒,防止發生併發操作問題。

事件驅動模型

整個服務端的流程處理,建立於事件機制上。在 [接受連線->讀->業務處理->寫 ->關閉連線 ]這個過程中,觸發器將觸發相應事件,由事件處理器對相應事件分別響應,完成伺服器端的業務處理。

事件定義

  1. onRead:當客戶端發來資料,並已被工作執行緒正確讀取時,觸發該事件 。該事件通知各事件處理器可以對客戶端發來的資料進行實際處理了。
  2. onWrite:當客戶端可以開始接受服務端傳送資料時觸發該事件,通過該事件,我們可以向客戶端傳送響應資料。(當前的實現中並未使用寫事件)
  3. onClosed:當客戶端與伺服器斷開連線時觸發該事件。

事件回撥機制的實現

在這個模型中,事件採用廣播方式,也就是所有註冊的事件處理器都能獲得事件通知。這樣可以將不同性質的業務處理,分別用不同的處理器實現,使每個處理器的功能儘可能單一。

如下圖:整個事件模型由監聽器、事件介面卡、事件觸發器(HandlerChain,PipeLine)、事件處理器組成。

event

  • ServerListener:這是一個事件介面,定義需監聽的伺服器事件
    interface ServerListener extends Serializable{
        /**
         * 可讀事件回撥
         * @param request
         */
        void onRead(ctx)
        /**
         * 可寫事件回撥
         * @param request
         * @param response
         */
        void onWrite(ctx)
        /**
         * 連線關閉回撥
         * @param request
         */
        void onClosed(ctx)
    }
  • EventAdapter:對 Serverlistener 介面實現一個介面卡 (EventAdapter),這樣的好處是最終的事件處理器可以只處理所關心的事件。
    class EventAdapter implements ServerListener {
        //下個處理器的引用
        protected next
        void onRead(Object ctx) {
        }
        void onWrite(Object ctx) {
        }
        void onClosed(Object ctx) {
        }
    }
  • Notifier:用於在適當的時候通過觸發伺服器事件,通知在冊的事件處理器對事件做出響應。
    interface Notifier extends Serializable{
        /**
         * 觸發所有可讀事件回撥
         */
        void fireOnRead(ctx)
        /**
         * 觸發所有可寫事件回撥
         */
        void fireOnWrite(ctx)
        /**
         * 觸發所有連線關閉事件回撥
         */
        void fireOnClosed(ctx)
    }
  • HandlerChain:實現了Notifier介面,維持有序的事件處理器鏈條,每次從第一個處理器開始觸發。
    class HandlerChain implements Notifier{
        EventAdapter head
        EventAdapter tail
        /**
         * 新增處理器到執行鏈的最後
         * @param handler
         */
        void addLast(handler) {
            if (tail != null) {
                tail.next = handler
                tail = tail.next
            } else {
                head = handler
                tail = head
            }
        }
        void fireOnRead(ctx) {
            head.onRead(ctx)
        }
        void fireOnWrite(ctx) {
            head.onWrite(ctx)
        }
        void fireOnClosed(ctx) {
            head.onClosed(ctx)
        }
    }
  • PipeLine:實現了Notifier介面,作為事件匯流排,維持一個事件鏈的列表。
    class PipeLine implements Notifier{
        static logger = LoggerFactory.getLogger(PipeLine.name)
        //監聽器佇列
        def listOfChain = []
        PipeLine(){}
        /**
         * 新增監聽器到監聽佇列中
         * @param chain
         */
        void addChain(chain) {
            synchronized (listOfChain) {
                if (!listOfChain.contains(chain)) {
                    listOfChain.add(chain)
                }
            }
        }
        /**
         * 觸發所有可讀事件回撥
         */
        void fireOnRead(ctx) {
            logger.debug("fireOnRead")
            listOfChain.each { chain ->
                chain.fireOnRead(ctx)
            }
        }
        /**
         * 觸發所有可寫事件回撥
         */
        void fireOnWrite(ctx) {
            listOfChain.each { chain ->
                chain.fireOnWrite(ctx)
            }
        }
        /**
         * 觸發所有連線關閉事件回撥
         */
        void fireOnClosed(ctx) {
            listOfChain.each { chain ->
                chain.fireOnClosed(ctx)
            }
        }
    }

事件處理流程

程式設計模型

程式設計模型

事件處理採用職責鏈模式,每個處理器處理完資料之後會決定是否繼續執行下一個處理器。如果處理器不將任務交給執行緒池處理,那麼整個處理流程都在同一個執行緒中處理。而且每個連線都有單獨的PipeLine,工作執行緒可以在多個連線上下文切換,但是一個連線上下文只會被一個執行緒處理。

核心類

ConnectionCtx

連線上下文ConnectionCtx

class ConnectionCtx {
    /**socket連線*/
    SocketChannel channel
    /**用於攜帶額外引數*/
    Object attachment
    /**處理當前連線的工作執行緒*/
    Worker worker
    /**連線超時時間*/
    Long timeout
    /**每個連線擁有自己的pipeline*/
    PipeLine pipeLine
}

NioServer

主執行緒負責監聽埠,持有工作執行緒的引用(使用輪轉法分配連線),每次有連線到來時,將連線放入工作執行緒的連線佇列,並喚醒執行緒selector.wakeup()(執行緒可能阻塞在selector上)。

class NioServer extends Thread {
    /**服務端的套接字通道*/
    ServerSocketChannel ssc
    /**選擇器*/
    Selector selector
    /**事件匯流排*/
    PipeLine pipeLine
    /**工作執行緒列表*/
    def workers = []
    /**當前工作執行緒索引*/
    int index
}

Worker

工作執行緒,負責註冊server傳遞過來的socket連線。主要監聽讀事件,管理socket,處理寫操作。

class Worker extends Thread {
    /**選擇器*/
    Selector selector
    /**讀緩衝區*/
    ByteBuffer buffer
    /**主執行緒分配的連線佇列*/
    def queue = []
    /**儲存按超時時間從小到大的連線*/
    TreeMap<Long, ConnectionCtx> ctxTreeMap

    void run() {
        while (true) {
            selector.select()
            //註冊主執行緒傳送過來的連線
            registerCtx()
            //關閉超時的連線
            closeTimeoutCtx()
            //處理事件
            dispatchEvent()
        }
    }
}

執行一個簡單的 Web 伺服器

我實現了一系列處理HTTP請求的處理器,具體實現看程式碼。

  • LineBasedDecoder:行解碼器,按行解析資料
  • HttpRequestDecoder:HTTP請求解析,目前只支援GET請求
  • HttpRequestHandler:Http 請求處理器,目前只支援GET方法
  • HttpResponseHandler:Http響應處理器

下面是寫在test中的例子

class WebServerTest {
    static void main(args) {
        def pipeLine = new PipeLine()

        def readChain = new HandlerChain()
        readChain.addLast(new LineBasedDecoder())
        readChain.addLast(new HttpRequestDecoder())
        readChain.addLast(new HttpRequestHandler())
        readChain.addLast(new HttpResponseHandler())

        def closeChain = new HandlerChain()
        closeChain.addLast(new ClosedHandler())

        pipeLine.addChain(readChain)
        pipeLine.addChain(closeChain)

        NioServer nioServer = new NioServer(pipeLine)
        nioServer.start()
    }
}

另外,還可以使用配置檔案getty.properties設定程式的執行引數。

#用於拼接訊息時使用的二進位制陣列的快取區
common_buffer_size=1024
#工作執行緒讀取tcp資料的快取大小
worker_rcv_buffer_size=1024
#監聽的埠
port=4399
#工作執行緒的數量
worker_num=1
#連線超時自動斷開時間
timeout=900
#根目錄
root=.

總結

Getty是我造的第二個小輪子,第一個是RedisHttpSession。都說不要重複造輪子。這話我是認同的,但是掌握一門技術最好的方法就是實踐,在沒有合適專案可以使用新技術的時候,造一個簡單的輪子是不錯的實踐手段。

Getty 的缺點或者說還可以優化的點:

  1. 執行緒的使用直接用了Thread類,看起來有點low。等以後水平提升了再來抽象一下。
  2. 目前只有讀事件是非同步的,寫事件是同步的。未來將寫事件也改為非同步的。

相關文章