前言
Getty是我爲了學習 Java NIO 所寫的一個 NIO 框架,實現過程中參考了 Netty 的設計,同時使用 Groovy 來實現。雖然只是玩具,但是麻雀雖小,五臟俱全,在實現過程中,不僅熟悉了 NIO 的使用,還借鑑了很多 Netty 的設計思想,提升了自己的編碼和設計能力。
至於爲什麼用 Groovy 來寫,因爲我剛學了 Groovy,正好拿來練手,加上 Groovy 是兼容 Java 的,所以只是語法上的差別,底層實現還是基於 Java API的。
Getty 的核心代碼行數不超過 500 行,一方面得益於 Groovy 簡潔的語法,另一方面是因爲我只實現了核心的邏輯,最複雜的其實是×××實現。腳手架容易搭,摩天大樓哪有那麼容易蓋,但用來學習 NIO 足以。
線程模型
有專門一個 NIO 線程- Acceptor 線程用於監聽服務端,接收客戶端的 TCP 連接請求,然後將連接分配給工作線程,由工作線程來監聽讀寫事件。
網絡 IO 操作-讀/寫等由多個工作線程負責,由這些工作線程負責消息的讀取、解碼、編碼和發送。
1 個工作線程可以同時處理N條鏈路,但是 1 個鏈路只對應 1 個工作線程,防止發生併發操作問題。
事件驅動模型
整個服務端的流程處理,建立於事件機制上。在 [接受連接->讀->業務處理->寫 ->關閉連接 ]這個過程中,觸發器將觸發相應事件,由事件處理器對相應事件分別響應,完成服務器端的業務處理。
事件定義
onRead
:當客戶端發來數據,並已被工作線程正確讀取時,觸發該事件 。該事件通知各事件處理器可以對客戶端發來的數據進行實際處理了。onWrite
:當客戶端可以開始接受服務端發送數據時觸發該事件,通過該事件,我們可以向客戶端發送響應數據。(當前的實現中並未使用寫事件)onClosed
:當客戶端與服務器斷開連接時觸發該事件。
事件回調機制的實現
在這個模型中,事件採用廣播方式,也就是所有註冊的事件處理器都能獲得事件通知。這樣可以將不同性質的業務處理,分別用不同的處理器實現,使每個處理器的功能儘可能單一。
如下圖:整個事件模型由監聽器、事件適配器、事件觸發器(HandlerChain,PipeLine)、事件處理器組成。
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 的缺點或者說還可以優化的點:
線程的使用直接用了
Thread
類,看起來有點low。等以後水平提升了再來抽象一下。目前只有讀事件是異步的,寫事件是同步的。未來將寫事件也改爲異步的。