netty實現原理淺析

Netty是JBoss出品的高效的Java NIO開發框架,關於其使用,可參考個人另外一篇文章 netty使用初步。本文將主要分析Netty實現方面的東西,因爲精力有限,本人並無對其源碼作了極細緻的研 究。若是下面的內容有錯誤或不嚴謹的地方,也請指正和諒解。對於Netty使用者來講,Netty提供了幾個典型的example,並有詳盡的API doc和guide doc,本文的一些內容及圖示也來自於Netty的文檔,特此致謝。html

一、整體結構

先放上一張漂亮的Netty整體結構圖,下面的內容也主要圍繞該圖上的一些核心功能作分析,但對如Container Integration及Security Support等高級可選功能,本文不予分析。java

二、網絡模型

Netty是典型的Reactor模型結構,關於Reactor的詳盡闡釋,可參考POSA2,這裏不作概念性的解釋。而應用Java NIO構建Reactor模式,Doug Lea(就是那位讓人無限景仰的大爺)在「Scalable IO in Java」中給了很好的闡述。這裏截取其PPT中經典的圖例說明 Reactor模式的典型實現:數據庫

一、這是最簡單的單Reactor單線程模型。Reactor線程是個多面手,負責多路分離套接字,Accept新鏈接,並分派請求處處理器鏈中。該模型 適用於處理器鏈中業務處理組件能快速完成的場景。不過,這種單線程模型不能充分利用多核資源,因此實際使用的很少。後端

二、相比上一種模型,該模型在處理器鏈部分採用了多線程(線程池),也是後端程序經常使用的模型。設計模式

三、 第三種模型比起第二種模型,是將Reactor分紅兩部分,mainReactor負責監聽server socket,accept新鏈接,並將創建的socket分派給subReactor。subReactor負責多路分離已鏈接的socket,讀寫網 絡數據,對業務處理功能,其扔給worker線程池完成。一般,subReactor個數上可與CPU個數等同。數組

說完Reacotr模型的三種形式,那麼Netty是哪一種呢?其實,我還有一種Reactor模型的變種沒說,那就是去掉線程池的第三種形式的變種,這也 是Netty NIO的默認模式。在實現上,Netty中的Boss類充當mainReactor,NioWorker類充當subReactor(默認 NioWorker的個數是Runtime.getRuntime().availableProcessors())。在處理新來的請求 時,NioWorker讀完已收到的數據到ChannelBuffer中,以後觸發ChannelPipeline中的ChannelHandler流。網絡

Netty是事件驅動的,能夠經過ChannelHandler鏈來控制執行流向。由於ChannelHandler鏈的執行過程是在 subReactor中同步的,因此若是業務處理handler耗時長,將嚴重影響可支持的併發數。這種模型適合於像Memcache這樣的應用場景,但 對須要操做數據庫或者和其餘模塊阻塞交互的系統就不是很合適。Netty的可擴展性很是好,而像ChannelHandler線程池化的須要,能夠經過在 ChannelPipeline中添加Netty內置的ChannelHandler實現類–ExecutionHandler實現,對使用者來講只是 添加一行代碼而已。對於ExecutionHandler須要的線程池模型,Netty提供了兩種可 選:1) MemoryAwareThreadPoolExecutor 可控制Executor中待處理任務的上限(超過上限時,後續進來的任務將被阻 塞),並可控制單個Channel待處理任務的上限;2) OrderedMemoryAwareThreadPoolExecutor 是  MemoryAwareThreadPoolExecutor 的子類,它還能夠保證同一Channel中處理的事件流的順序性,這主要是控制事件在異步處 理模式下可能出現的錯誤的事件順序,但它並不保證同一Channel中的事件都在一個線程中執行(一般也不必)。通常來 說,OrderedMemoryAwareThreadPoolExecutor 是個很不錯的選擇,固然,若是有須要,也能夠DIY一個。多線程

三、 buffer

org.jboss.netty.buffer包的接口及類的結構圖以下:併發

該包核心的接口是ChannelBuffer和ChannelBufferFactory,下面予以簡要的介紹。框架

Netty使用ChannelBuffer來存儲並操做讀寫的網絡數據。ChannelBuffer除了提供和ByteBuffer相似的方法,還提供了 一些實用方法,具體可參考其API文檔。ChannelBuffer的實現類有多個,這裏列舉其中主要的幾個:

1)HeapChannelBuffer:這是Netty讀網絡數據時默認使用的ChannelBuffer,這裏的Heap就是Java堆的意思,由於 讀SocketChannel的數據是要通過ByteBuffer的,而ByteBuffer實際操做的就是個byte數組,因此 ChannelBuffer的內部就包含了一個byte數組,使得ByteBuffer和ChannelBuffer之間的轉換是零拷貝方式。根據網絡字 節續的不一樣,HeapChannelBuffer又分爲BigEndianHeapChannelBuffer和 LittleEndianHeapChannelBuffer,默認使用的是BigEndianHeapChannelBuffer。Netty在讀網絡 數據時使用的就是HeapChannelBuffer,HeapChannelBuffer是個大小固定的buffer,爲了避免至於分配的Buffer的 大小不太合適,Netty在分配Buffer時會參考上次請求須要的大小。

2)DynamicChannelBuffer:相比於HeapChannelBuffer,DynamicChannelBuffer可動態自適應大 小。對於在DecodeHandler中的寫數據操做,在數據大小未知的狀況下,一般使用DynamicChannelBuffer。

3)ByteBufferBackedChannelBuffer:這是directBuffer,直接封裝了ByteBuffer的 directBuffer。

對於讀寫網絡數據的buffer,分配策略有兩種:1)一般出於簡單考慮,直接分配固定大小的buffer,缺點是,對一些應用來講這個大小限制有時是不 合理的,而且若是buffer的上限很大也會有內存上的浪費。2)針對固定大小的buffer缺點,就引入動態buffer,動態buffer之於固定 buffer至關於List之於Array。

buffer的寄存策略常見的也有兩種(實際上是我知道的就限於此):1)在多線程(線程池) 模型下,每一個線程維護本身的讀寫buffer,每次處理新的請求前清空buffer(或者在處理結束後清空),該請求的讀寫操做都須要在該線程中完成。 2)buffer和socket綁定而與線程無關。兩種方法的目的都是爲了重用buffer。

Netty對buffer的處理策略是:讀 請求數據時,Netty首先讀數據到新建立的固定大小的HeapChannelBuffer中,當HeapChannelBuffer滿或者沒有數據可讀 時,調用handler來處理數據,這一般首先觸發的是用戶自定義的DecodeHandler,由於handler對象是和ChannelSocket 綁定的,因此在DecodeHandler裏能夠設置ChannelBuffer成員,當解析數據包發現數據不完整時就終止這次處理流程,等下次讀事件觸 發時接着上次的數據繼續解析。就這個過程來講,和ChannelSocket綁定的DecodeHandler中的Buffer一般是動態的可重用 Buffer(DynamicChannelBuffer),而在NioWorker中讀ChannelSocket中的數據的buffer是臨時分配的 固定大小的HeapChannelBuffer,這個轉換過程是有個字節拷貝行爲的。

對ChannelBuffer的建立,Netty內部使用的是ChannelBufferFactory接口,具體的實現有 DirectChannelBufferFactory和HeapChannelBufferFactory。對於開發者建立 ChannelBuffer,可以使用實用類ChannelBuffers中的工廠方法。

四、Channel

和Channel相關的接口及類結構圖以下:

從該結構圖也能夠看到,Channel主要提供的功能以下:

1)當前Channel的狀態信息,好比是打開仍是關閉等。
2)經過ChannelConfig能夠獲得的Channel配置信息。
3)Channel所支持的如read、write、bind、connect等IO操做。
4)獲得處理該Channel的ChannelPipeline,既而能夠調用其作和請求相關的IO操做。

在Channel實現方面,以一般使用的nio socket來講,Netty中的NioServerSocketChannel和NioSocketChannel分別封裝了java.nio中包含的 ServerSocketChannel和SocketChannel的功能。

五、ChannelEvent

如前所述,Netty是事件驅動的,其經過ChannelEvent來肯定事件流的方向。一個ChannelEvent是依附於Channel的 ChannelPipeline來處理,並由ChannelPipeline調用ChannelHandler來作具體的處理。下面是和 ChannelEvent相關的接口及類圖:

對於使用者來講,在ChannelHandler實現類中會使用繼承於ChannelEvent的MessageEvent,調用其 getMessage()方法來得到讀到的ChannelBuffer或被轉化的對象。

六、ChannelPipeline

Netty 在事件處理上,是經過ChannelPipeline來控制事件流,經過調用註冊其上的一系列ChannelHandler來處理事件,這也是典型的攔截 器模式。下面是和ChannelPipeline相關的接口及類圖:

事件流有兩種,upstream事件和downstream事件。在ChannelPipeline中,其可被註冊的ChannelHandler既能夠 是 ChannelUpstreamHandler 也能夠是ChannelDownstreamHandler ,但事件在ChannelPipeline傳遞過程當中只會調用匹配流的ChannelHandler。在事件流的過濾器鏈 中,ChannelUpstreamHandler或ChannelDownstreamHandler既能夠終止流程,也能夠經過調用 ChannelHandlerContext.sendUpstream(ChannelEvent)或 ChannelHandlerContext.sendDownstream(ChannelEvent)將事件傳遞下去。下面是事件流處理的圖示:

從上圖可見,upstream event是被Upstream Handler們自底向上逐個處理,downstream event是被Downstream Handler們自頂向下逐個處理,這裏的上下關係就是向ChannelPipeline裏添加Handler的前後順序關係。簡單的理 解,upstream event是處理來自外部的請求的過程,而downstream event是處理向外發送請求的過程。

服務端處 理請求的過程一般就是解碼請求、業務邏輯處理、編碼響應,構建的ChannelPipeline也就相似下面的代碼片段:

1

2

3

4

ChannelPipeline pipeline = Channels.pipeline();

pipeline.addLast("decoder", new MyProtocolDecoder());

pipeline.addLast("encoder", new MyProtocolEncoder());

pipeline.addLast("handler", new MyBusinessLogicHandler());

其中,MyProtocolDecoder是ChannelUpstreamHandler類型,MyProtocolEncoder是 ChannelDownstreamHandler類型,MyBusinessLogicHandler既能夠是 ChannelUpstreamHandler類型,也可兼ChannelDownstreamHandler類型,視其是服務端程序仍是客戶端程序以及 應用須要而定。

補充一點,Netty對抽象和實現作了很好的解耦。像org.jboss.netty.channel.socket包, 定義了一些和socket處理相關的接口,而org.jboss.netty.channel.socket.nio、 org.jboss.netty.channel.socket.oio等包,則是和協議相關的實現。

七、codec framework

對於請求協議的編碼解碼,固然是能夠按照協議格式本身操做ChannelBuffer中的字節數據。另外一方面,Netty也作了幾個很實用的codec helper,這裏給出簡單的介紹。

1)FrameDecoder:FrameDecoder內部維護了一個 DynamicChannelBuffer成員來存儲接收到的數據,它就像個抽象模板,把整個解碼過程模板寫好了,其子類只需實現decode函數便可。 FrameDecoder的直接實現類有兩個:(1)DelimiterBasedFrameDecoder是基於分割符 (好比\r\n)的解碼器,可在構造函數中指定分割符。(2)LengthFieldBasedFrameDecoder是基於長度字段的解碼器。若是協 議 格式相似「內容長度」+內容、「固定頭」+「內容長度」+動態內容這樣的格式,就可使用該解碼器,其使用方法在API DOC上詳盡的解釋。
2)ReplayingDecoder: 它是FrameDecoder的一個變種子類,它相對於FrameDecoder是非阻塞解碼。也就是說,使用 FrameDecoder時須要考慮到讀到的數據有多是不完整的,而使用ReplayingDecoder就能夠假定讀到了所有的數據。
3)ObjectEncoder 和ObjectDecoder:編碼解碼序列化的Java對象。
4)HttpRequestEncoder和 HttpRequestDecoder:http協議處理。

下面來看使用FrameDecoder和ReplayingDecoder的兩個例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class IntegerHeaderFrameDecoder extends FrameDecoder {

    protected Object decode(ChannelHandlerContext ctx, Channel channel,

                ChannelBuffer buf) throws Exception {

        if (buf.readableBytes() < 4) {

            return null;

        }

        buf.markReaderIndex();

        int length = buf.readInt();

        if (buf.readableBytes() < length) {

            buf.resetReaderIndex();

            return null;

        }

        return buf.readBytes(length);

    }

}

而使用ReplayingDecoder的解碼片段相似下面的,相對來講會簡化不少。

1

2

3

4

5

6

public class IntegerHeaderFrameDecoder2 extends ReplayingDecoder {

    protected Object decode(ChannelHandlerContext ctx, Channel channel,

            ChannelBuffer buf, VoidEnum state) throws Exception {

        return buf.readBytes(buf.readInt());

    }

}

就實現來講,當在ReplayingDecoder子類的decode函數中調用ChannelBuffer讀數據時,若是讀失敗,那麼 ReplayingDecoder就會catch住其拋出的Error,而後ReplayingDecoder接手控制權,等待下一次讀到後續的數據後繼 續decode。

八、小結

儘管該文行至此處將止,但該文顯然沒有將Netty實現原理深刻淺出的說全說透。當我打算寫這篇文章時,也是一邊看Netty的代碼,一邊總結些可寫的東 西,但先後斷斷續續,到最後都沒了多少興致。我仍是愛作一些源碼分析的事情,但精力終究有限,而且倘不能把源碼分析的結果有條理的托出來,不能產生有意義 的心得,這分析也沒什麼價值和趣味。而就分析Netty代碼的感覺來講,Netty的代碼很漂亮,結構上層次上很清晰,不過這種面向接口及抽象層次對代碼 跟蹤非常個問題,由於跟蹤代碼常常遇到接口和抽象類,只能藉助於工廠類和API DOC,反覆對照接口和實現類的對應關係。就像幾乎任何優秀的Java開源項目都會用上一系列優秀的設計模式,也徹底能夠從模式這一點單獨拿出一篇分析文 章來,儘管我目前沒有這樣的想法。而在此文完成以後,我也沒什麼興趣再看Netty的代碼了。

50

相關文章
相關標籤/搜索