【Getty】Java NIO框架設計與實現

前言

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

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

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

線程模型

Getty 使用的是 Reactor 多線程模型
reactorreactor

  1. 有專門一個 NIO 線程- Acceptor 線程用於監聽服務端,接收客戶端的 TCP 連接請求,然後將連接分配給工作線程,由工作線程來監聽讀寫事件。

  2. 網絡 IO 操作-讀/寫等由多個工作線程負責,由這些工作線程負責消息的讀取、解碼、編碼和發送。

  3. 1 個工作線程可以同時處理N條鏈路,但是 1 個鏈路只對應 1 個工作線程,防止發生併發操作問題。

事件驅動模型

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

事件定義

  1. onRead:當客戶端發來數據,並已被工作線程正確讀取時,觸發該事件 。該事件通知各事件處理器可以對客戶端發來的數據進行實際處理了。

  2. onWrite:當客戶端可以開始接受服務端發送數據時觸發該事件,通過該事件,我們可以向客戶端發送響應數據。(當前的實現中並未使用寫事件)

  3. onClosed:當客戶端與服務器斷開連接時觸發該事件。

事件回調機制的實現

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

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

  • 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 p_w_upload
    /**處理當前連接的工作線程*/
    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. 目前只有讀事件是異步的,寫事件是同步的。未來將寫事件也改爲異步的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章