Netty編解碼框架分析

fpcm 9年前發布 | 57K 次閱讀 Netty 網絡工具包

1. 背景

1.1. 編解碼技術

通常我們也習慣將編碼(Encode)稱為序列化(serialization),它將對象序列化為字節數組,用于網絡傳輸、數據持久化或者其它用途。

反之,解碼(Decode)/反序列化(deserialization)把從網絡、磁盤等讀取的字節數組還原成原始對象(通常是原始對象的拷貝),以方便后續的業務邏輯操作。

進行遠程跨進程服務調用時(例如RPC調用),需要使用特定的編解碼技術,對需要進行網絡傳輸的對象做編碼或者解碼,以便完成遠程調用。

1.2. 常用的編解碼框架

1.2.1. Java序列化

相信大多數Java程序員接觸到的第一種序列化或者編解碼技術就是Java默認提供的序列化機制,需要序列化的Java對象只需要實現 java.io.Serializable接口并生成序列化ID,這個類就能夠通過java.io.ObjectInput和 java.io.ObjectOutput序列化和反序列化。

由于使用簡單,開發門檻低,Java序列化得到了廣泛的應用,但是由于它自身存在很多缺點,因此大多數的RPC框架并沒有選擇它。Java序列化的主要缺點如下:

1) 無法跨語言:是Java序列化最致命的問題。對于跨進程的服務調用,服務提供者可能會使用C++或者其它語言開發,當我們需要和異構語言進程交互 時,Java序列化就難以勝任。由于Java序列化技術是Java語言內部的私有協議,其它語言并不支持,對于用戶來說它完全是黑盒。Java序列化后的 字節數組,別的語言無法進行反序列化,這就嚴重阻礙了它的應用范圍;

2) 序列化后的碼流太大: 例如使用二進制編解碼技術對同一個復雜的POJO對象進行編碼,它的碼流僅僅為Java序列化之后的20%左右;目前主流的編解碼框架,序列化之后的碼流都遠遠小于原生的Java序列化;

3) 序列化效率差:在相同的硬件條件下、對同一個POJO對象做100W次序列化,二進制編碼和Java原生序列化的性能對比測試如下圖所示:Java原生序列化的耗時是二進制編碼的16.2倍,效率非常差。

Netty編解碼框架分析

圖1-1 二進制編碼和Java原生序列化性能對比

1.2.2. Google的Protobuf

Protobuf全稱Google Protocol Buffers,它由谷歌開源而來,在谷歌內部久經考驗。它將數據結構以.proto文件進行描述,通過代碼生成工具可以生成對應數據結構的POJO對象和Protobuf相關的方法和屬性。

它的特點如下:

1) 結構化數據存儲格式(XML,JSON等);

2) 高效的編解碼性能;

3) 語言無關、平臺無關、擴展性好;

4) 官方支持Java、C++和Python三種語言。

首先我們來看下為什么不使用XML,盡管XML的可讀性和可擴展性非常好,也非常適合描述數據結構,但是XML解析的時間開銷和XML為了可讀性而犧牲的空間開銷都非常大,因此不適合做高性能的通信協議。Protobuf使用二進制編碼,在空間和性能上具有更大的優勢。

Protobuf另一個比較吸引人的地方就是它的數據描述文件和代碼生成機制,利用數據描述文件對數據結構進行說明的優點如下:

1) 文本化的數據結構描述語言,可以實現語言和平臺無關,特別適合異構系統間的集成;

2) 通過標識字段的順序,可以實現協議的前向兼容;

3) 自動代碼生成,不需要手工編寫同樣數據結構的C++和Java版本;

4) 方便后續的管理和維護。相比于代碼,結構化的文檔更容易管理和維護。

1.2.3. Apache的Thrift

Thrift源于非死book,在2007年非死book將Thrift作為一個開源項目提交給Apache基金會。對于當時的 非死book來說,創造Thrift是為了解決非死book各系統間大數據量的傳輸通信以及系統之間語言環境不同需要跨平臺的特性,因此 Thrift可以支持多種程序語言,如C++、C#、Cocoa、Erlang、Haskell、Java、Ocami、Perl、PHP、 Python、Ruby和Smalltalk。

在多種不同的語言之間通信,Thrift可以作為高性能的通信中間件使用,它支持數據(對象)序列化和多種類型的RPC服務。Thrift適用于靜 態的數據交換,需要先確定好它的數據結構,當數據結構發生變化時,必須重新編輯IDL文件,生成代碼和編譯,這一點跟其他IDL工具相比可以視為是 Thrift的弱項。Thrift適用于搭建大型數據交換及存儲的通用工具,對于大型系統中的內部數據傳輸,相對于JSON和XML在性能和傳輸大小上都 有明顯的優勢。

Thrift主要由5部分組成:

1) 語言系統以及IDL編譯器:負責由用戶給定的IDL文件生成相應語言的接口代碼;

2) TProtocol:RPC的協議層,可以選擇多種不同的對象序列化方式,如JSON和Binary;

3) TTransport:RPC的傳輸層,同樣可以選擇不同的傳輸層實現,如socket、NIO、MemoryBuffer等;

4) TProcessor:作為協議層和用戶提供的服務實現之間的紐帶,負責調用服務實現的接口;

5) TServer:聚合TProtocol、TTransport和TProcessor等對象。

我們重點關注的是編解碼框架,與之對應的就是TProtocol。由于Thrift的RPC服務調用和編解碼框架綁定在一起,所以,通常我們使用Thrift的時候會采取RPC框架的方式。但是,它的TProtocol編解碼框架還是可以以類庫的方式獨立使用的。

與Protobuf比較類似的是,Thrift通過IDL描述接口和數據結構定義,它支持8種Java基本類型、Map、Set和List,支持可選和必選定義,功能非常強大。因為可以定義數據結構中字段的順序,所以它也可以支持協議的前向兼容。

Thrift支持三種比較典型的編解碼方式:

1) 通用的二進制編解碼;

2) 壓縮二進制編解碼;

3) 優化的可選字段壓縮編解碼。

由于支持二進制壓縮編解碼,Thrift的編解碼性能表現也相當優異,遠遠超過Java序列化和RMI等。

1.2.4. JBoss Marshalling

JBoss Marshalling是一個Java對象的序列化API包,修正了JDK自帶的序列化包的很多問題,但又保持跟java.io.Serializable接口的兼容;同時增加了一些可調的參數和附加的特性,并且這些參數和特性可通過工廠類進行配置。

相比于傳統的Java序列化機制,它的優點如下:

1) 可插拔的類解析器,提供更加便捷的類加載定制策略,通過一個接口即可實現定制;

2) 可插拔的對象替換技術,不需要通過繼承的方式;

3) 可插拔的預定義類緩存表,可以減小序列化的字節數組長度,提升常用類型的對象序列化性能;

4) 無須實現java.io.Serializable接口,即可實現Java序列化;

5) 通過緩存技術提升對象的序列化性能。

相比于前面介紹的兩種編解碼框架,JBoss Marshalling更多是在JBoss內部使用,應用范圍有限。

1.2.5. 其它編解碼框架

除了上述介紹的編解碼框架和技術之外,比較常用的還有MessagePack、kryo、hession和Json等。限于篇幅所限,不再一一枚舉,感興趣的朋友可以自行查閱相關資料學習。

2. Netty編解碼框架

2.1. Netty為什么要提供編解碼框架

作為一個高性能的異步、NIO通信框架,編解碼框架是Netty的重要組成部分。盡管站在微內核的角度看,編解碼框架并不是Netty微內核的組成部分,但是通過ChannelHandler定制擴展出的編解碼框架卻是不可或缺的。

下面我們從幾個角度詳細談下這個話題,首先一起看下Netty的邏輯架構圖:

Netty編解碼框架分析

圖2-1 Netty邏輯架構圖

從網絡讀取的inbound消息,需要經過解碼,將二進制的數據報轉換成應用層協議消息或者業務消息,才能夠被上層的應用邏輯識別和處理;同理,用 戶發送到網絡的outbound業務消息,需要經過編碼轉換成二進制字節數組(對于Netty就是ByteBuf)才能夠發送到網絡對端。編碼和解碼功能 是NIO框架的有機組成部分,無論是由業務定制擴展實現,還是NIO框架內置編解碼能力,該功能是必不可少的。

為了降低用戶的開發難度,Netty對常用的功能和API做了裝飾,以屏蔽底層的實現細節。編解碼功能的定制,對于熟悉Netty底層實現的開發者 而言,直接基于ChannelHandler擴展開發,難度并不是很大。但是對于大多數初學者或者不愿意去了解底層實現細節的用戶,需要提供給他們更簡單 的類庫和API,而不是ChannelHandler。

Netty在這方面做得非常出色,針對編解碼功能,它既提供了通用的編解碼框架供用戶擴展,又提供了常用的編解碼類庫供用戶直接使用。在保證定制擴展性的基礎之上,盡量降低用戶的開發工作量和開發門檻,提升開發效率。

Netty預置的編解碼功能列表如下:base64、Protobuf、JBoss Marshalling、spdy等。

Netty編解碼框架分析

圖2-2 Netty預置的編解碼功能列表

2.2. 常用的解碼器

2.2.1. LineBasedFrameDecoder解碼器

LineBasedFrameDecoder是回車換行解碼器,如果用戶發送的消息以回車換行符作為消息結束的標識,則可以直接使用Netty的 LineBasedFrameDecoder對消息進行解碼,只需要在初始化Netty服務端或者客戶端時將LineBasedFrameDecoder 正確的添加到ChannelPipeline中即可,不需要自己重新實現一套換行解碼器。

LineBasedFrameDecoder的工作原理是它依次遍歷ByteBuf中的可讀字節,判斷看是否有“\n”或者“\r\n”,如果有, 就以此位置為結束位置,從可讀索引到結束位置區間的字節就組成了一行。它是以換行符為結束標志的解碼器,支持攜帶結束符或者不攜帶結束符兩種解碼方式,同 時支持配置單行的最大長度。如果連續讀取到最大長度后仍然沒有發現換行符,就會拋出異常,同時忽略掉之前讀到的異常碼流。防止由于數據報沒有攜帶換行符導 致接收到ByteBuf無限制積壓,引起系統內存溢出。

它的使用效果如下:

解碼之前:
+------------------------------------------------------------------+
                        接收到的數據報
“This is a netty example for using the nio framework.\r\n When you“
+------------------------------------------------------------------+
解碼之后的ChannelHandler接收到的Object如下:
+------------------------------------------------------------------+
                        解碼之后的文本消息
“This is a netty example for using the nio framework.“
+------------------------------------------------------------------+

通常情況下,LineBasedFrameDecoder會和StringDecoder配合使用,組合成按行切換的文本解碼器,對于文本類協議的解析,文本換行解碼器非常實用,例如對HTTP消息頭的解析、FTP協議消息的解析等。

下面我們簡單給出文本換行解碼器的使用示例:

@Override
protected void initChannel(SocketChannel arg0) throws Exception {
   arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
   arg0.pipeline().addLast(new StringDecoder());
   arg0.pipeline().addLast(new UserServerHandler());
}

初始化Channel的時候,首先將LineBasedFrameDecoder添加到ChannelPipeline中,然后再依次添加字符串解碼器StringDecoder,業務Handler。

2.2.2. DelimiterBasedFrameDecoder解碼器

DelimiterBasedFrameDecoder是分隔符解碼器,用戶可以指定消息結束的分隔符,它可以自動完成以分隔符作為碼流結束標識的消息的解碼。回車換行解碼器實際上是一種特殊的DelimiterBasedFrameDecoder解碼器。

分隔符解碼器在實際工作中也有很廣泛的應用,筆者所從事的電信行業,很多簡單的文本私有協議,都是以特殊的分隔符作為消息結束的標識,特別是對于那些使用長連接的基于文本的私有協議。

分隔符的指定:與大家的習慣不同,分隔符并非以char或者string作為構造參數,而是ByteBuf,下面我們就結合實際例子給出它的用法。

假如消息以“$_”作為分隔符,服務端或者客戶端初始化ChannelPipeline的代碼實例如下:

@Override
public void initChannel(SocketChannel ch)
    throws Exception {
    ByteBuf delimiter = Unpooled.copiedBuffer("$_"
        .getBytes());
   ch.pipeline().addLast(
        new DelimiterBasedFrameDecoder(1024,
            delimiter));
   ch.pipeline().addLast(new StringDecoder());
   ch.pipeline().addLast(new UserServerHandler());
}

首先將“$_”轉換成ByteBuf對象,作為參數構造DelimiterBasedFrameDecoder,將其添加到 ChannelPipeline中,然后依次添加字符串解碼器(通常用于文本解碼)和用戶Handler,請注意解碼器和Handler的添加順序,如果 順序顛倒,會導致消息解碼失敗。

DelimiterBasedFrameDecoder原理分析:解碼時,判斷當前已經讀取的ByteBuf中是否包含分隔符ByteBuf,如果包含,則截取對應的ByteBuf返回,源碼如下:

Netty編解碼框架分析

詳細分析下indexOf(buffer, delim)方法的實現,代碼如下:

Netty編解碼框架分析

該算法與Java String中的搜索算法類似,對于原字符串使用兩個指針來進行搜索,如果搜索成功,則返回索引位置,否則返回-1。

2.2.3. FixedLengthFrameDecoder解碼器

FixedLengthFrameDecoder是固定長度解碼器,它能夠按照指定的長度對消息進行自動解碼,開發者不需要考慮TCP的粘包/拆包等問題,非常實用。

對于定長消息,如果消息實際長度小于定長,則往往會進行補位操作,它在一定程度上導致了空間和資源的浪費。但是它的優點也是非常明顯的,編解碼比較簡單,因此在實際項目中仍然有一定的應用場景。

利用FixedLengthFrameDecoder解碼器,無論一次接收到多少數據報,它都會按照構造函數中設置的固定長度進行解碼,如果是半包消息,FixedLengthFrameDecoder會緩存半包消息并等待下個包到達后進行拼包,直到讀取到一個完整的包。

假如單條消息的長度是20字節,使用FixedLengthFrameDecoder解碼器的效果如下:

解碼前:
+------------------------------------------------------------------+
                        接收到的數據報
“HELLO NETTY FOR USER DEVELOPER“
+------------------------------------------------------------------+
解碼后:
+------------------------------------------------------------------+
                        解碼后的數據報
“HELLO NETTY FOR USER“
+------------------------------------------------------------------+

2.2.4. LengthFieldBasedFrameDecoder解碼器

了解TCP通信機制的讀者應該都知道TCP底層的粘包和拆包,當我們在接收消息的時候,顯示不能認為讀取到的報文就是個整包消息,特別是對于采用非阻塞I/O和長連接通信的程序。

如何區分一個整包消息,通常有如下4種做法:

1) 固定長度,例如每120個字節代表一個整包消息,不足的前面補位。解碼器在處理這類定常消息的時候比較簡單,每次讀到指定長度的字節后再進行解碼;

2) 通過回車換行符區分消息,例如HTTP協議。這類區分消息的方式多用于文本協議;

3) 通過特定的分隔符區分整包消息;

4) 通過在協議頭/消息頭中設置長度字段來標識整包消息。

前三種解碼器之前的章節已經做了詳細介紹,下面讓我們來一起學習最后一種通用解碼器-LengthFieldBasedFrameDecoder。

大多數的協議(私有或者公有),協議頭中會攜帶長度字段,用于標識消息體或者整包消息的長度,例如SMPP、HTTP協議等。由于基于長度解碼需求 的通用性,以及為了降低用戶的協議開發難度,Netty提供了LengthFieldBasedFrameDecoder,自動屏蔽TCP底層的拆包和粘 包問題,只需要傳入正確的參數,即可輕松解決“讀半包“問題。

下面我們看看如何通過參數組合的不同來實現不同的“半包”讀取策略。第一種常用的方式是消息的第一個字段是長度字段,后面是消息體,消息頭中只包含一個長度字段。它的消息結構定義如圖所示:

Netty編解碼框架分析

圖2-3 解碼前的字節緩沖區(14字節)

使用以下參數組合進行解碼:

1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 0。

解碼后的字節緩沖區內容如圖所示:

Netty編解碼框架分析

圖2-4 解碼后的字節緩沖區(14字節)

通過ByteBuf.readableBytes()方法我們可以獲取當前消息的長度,所以解碼后的字節緩沖區可以不攜帶長度字段,由于長度字段在起始位置并且長度為2,所以將initialBytesToStrip設置為2,參數組合修改為:

1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 2。

解碼后的字節緩沖區內容如圖所示:

Netty編解碼框架分析

圖2-5 跳過長度字段解碼后的字節緩沖區(12字節)

解碼后的字節緩沖區丟棄了長度字段,僅僅包含消息體,對于大多數的協議,解碼之后消息長度沒有用處,因此可以丟棄。

在大多數的應用場景中,長度字段僅用來標識消息體的長度,這類協議通常由消息長度字段+消息體組成,如上圖所示的幾個例子。但是,對于某些協議,長 度字段還包含了消息頭的長度。在這種應用場景中,往往需要使用lengthAdjustment進行修正。由于整個消息(包含消息頭)的長度往往大于消息 體的長度,所以,lengthAdjustment為負數。圖2-6展示了通過指定lengthAdjustment字段來包含消息頭的長度:

1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = -2;

4) initialBytesToStrip = 0。

解碼之前的碼流:

Netty編解碼框架分析

圖2-6 包含長度字段自身的碼流

解碼之后的碼流:

Netty編解碼框架分析

圖2-7 解碼后的碼流

由于協議種類繁多,并不是所有的協議都將長度字段放在消息頭的首位,當標識消息長度的字段位于消息頭的中間或者尾部時,需要使用lengthFieldOffset字段進行標識,下面的參數組合給出了如何解決消息長度字段不在首位的問題:

1) lengthFieldOffset = 2;

2) lengthFieldLength = 3;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 0。

其中lengthFieldOffset表示長度字段在消息頭中偏移的字節數,lengthFieldLength 表示長度字段自身的長度,解碼效果如下:

解碼之前:

Netty編解碼框架分析

圖2-8 長度字段偏移的原始碼流

解碼之后:

Netty編解碼框架分析

圖2-9長度字段偏移解碼后的碼流

由于消息頭1的長度為2,所以長度字段的偏移量為2;消息長度字段Length為3,所以lengthFieldLength值為3。由于長度字段僅僅標識消息體的長度,所以lengthAdjustment和initialBytesToStrip都為0。

最后一種場景是長度字段夾在兩個消息頭之間或者長度字段位于消息頭的中間,前后都有其它消息頭字段,在這種場景下如果想忽略長度字段以及其前面的其它消息頭字段,則可以通過initialBytesToStrip參數來跳過要忽略的字節長度,它的組合配置示意如下:

1) lengthFieldOffset = 1;

2) lengthFieldLength = 2;

3) lengthAdjustment = 1;

4) initialBytesToStrip = 3。

解碼之前的碼流(16字節):

Netty編解碼框架分析

圖2-10長度字段夾在消息頭中間的原始碼流(16字節)

解碼之后的碼流(13字節):

Netty編解碼框架分析

圖2-11長度字段夾在消息頭中間解碼后的碼流(13字節)

由于HDR1的長度為1,所以長度字段的偏移量lengthFieldOffset為1;長度字段為2個字節,所以 lengthFieldLength為2。由于長度字段是消息體的長度,解碼后如果攜帶消息頭中的字段,則需要使用lengthAdjustment進行 調整,此處它的值為1,代表的是HDR2的長度,最后由于解碼后的緩沖區要忽略長度字段和HDR1部分,所以lengthAdjustment為3。解碼 后的結果為13個字節,HDR1和Length字段被忽略。

事實上,通過4個參數的不同組合,可以達到不同的解碼效果,用戶在使用過程中可以根據業務的實際情況進行靈活調整。

由于TCP存在粘包和組包問題,所以通常情況下用戶需要自己處理半包消息。利用LengthFieldBasedFrameDecoder解碼器可以自動解決半包問題,它的習慣用法如下:

pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65536,0,2));
pipeline.addLast("UserDecoder", new UserDecoder());

在pipeline中增加LengthFieldBasedFrameDecoder解碼器,指定正確的參數組合,它可以將Netty的 ByteBuf解碼成整包消息,后面的用戶解碼器拿到的就是個完整的數據報,按照邏輯正常進行解碼即可,不再需要額外考慮“讀半包”問題,降低了用戶的開 發難度。

2.3. 常用的編碼器

Netty并沒有提供與2.2章節匹配的編碼器,原因如下:

1) 2.2章節介紹的4種常用的解碼器本質都是解析一個完整的數據報給后端,主要用于解決TCP底層粘包和拆包;對于編碼,就是將POJO對象序列化為 ByteBuf,不需要與TCP層面打交道,也就不存在半包編碼問題。從應用場景和需要解決的實際問題角度看,雙方是非對等的;

2) 很難抽象出合適的編碼器,對于不同的用戶和應用場景,序列化技術不盡相同,在Netty底層統一抽象封裝也并不合適。

Netty默認提供了豐富的編解碼框架供用戶集成使用,本文對較常用的Java序列化編碼器進行講解。其它的編碼器,實現方式大同小異。

2.3.1. ObjectEncoder編碼器

ObjectEncoder是Java序列化編碼器,它負責將實現Serializable接口的對象序列化為byte [],然后寫入到ByteBuf中用于消息的跨網絡傳輸。

下面我們一起分析下它的實現:

首先,我們發現它繼承自MessageToByteEncoder,它的作用就是將對象編碼成ByteBuf:

Netty編解碼框架分析

如果要使用Java序列化,對象必須實現Serializable接口,因此,它的泛型類型為Serializable。

MessageToByteEncoder的子類只需要實現encode(ChannelHandlerContext ctx, I msg, ByteBuf out)方法即可,下面我們重點關注encode方法的實現:

Netty編解碼框架分析

首先創建ByteBufOutputStream和ObjectOutputStream,用于將Object對象序列化到ByteBuf中,值得注意的是在writeObject之前需要先將長度字段(4個字節)預留,用于后續長度字段的更新。

依次寫入長度占位符(4字節)、序列化之后的Object對象,之后根據ByteBuf的writeIndex計算序列化之后的碼流長度,最后調用ByteBuf的setInt(int index, int value)更新長度占位符為實際的碼流長度。

有個細節需要注意,更新碼流長度字段使用了setInt方法而不是writeInt,原因就是setInt方法只更新內容,并不修改readerIndex和writerIndex。

3. Netty編解碼框架可定制性

盡管Netty預置了豐富的編解碼類庫功能,但是在實際的業務開發過程中,總是需要對編解碼功能做一些定制。使用Netty的編解碼框架,可以非常方便的進行協議定制。本章節將對常用的支持定制的編解碼類庫進行講解,以期讓讀者能夠盡快熟悉和掌握編解碼框架。

3.1. 解碼器

3.1.1. ByteToMessageDecoder抽象解碼器

使用NIO進行網絡編程時,往往需要將讀取到的字節數組或者字節緩沖區解碼為業務可以使用的POJO對象。為了方便業務將ByteBuf解碼成業務POJO對象,Netty提供了ByteToMessageDecoder抽象工具解碼類。

用戶自定義解碼器繼承ByteToMessageDecoder,只需要實現void decode(ChannelHandler Context ctx, ByteBuf in, List<Object> out)抽象方法即可完成ByteBuf到POJO對象的解碼。

由于ByteToMessageDecoder并沒有考慮TCP粘包和拆包等場景,用戶自定義解碼器需要自己處理“讀半包”問題。正因為如此,大多數場景不會直接繼承ByteToMessageDecoder,而是繼承另外一些更高級的解碼器來屏蔽半包的處理。

實際項目中,通常將LengthFieldBasedFrameDecoder和ByteToMessageDecoder組合使用,前者負責將網絡讀取的數據報解碼為整包消息,后者負責將整包消息解碼為最終的業務對象。

除了和其它解碼器組合形成新的解碼器之外,ByteToMessageDecoder也是很多基礎解碼器的父類,它的繼承關系如下圖所示:

Netty編解碼框架分析

圖3-1 ByteToMessageDecoder繼承關系圖

3.1.2. MessageToMessageDecoder抽象解碼器

MessageToMessageDecoder實際上是Netty的二次解碼器,它的職責是將一個對象二次解碼為其它對象。

為什么稱它為二次解碼器呢?我們知道,從SocketChannel讀取到的TCP數據報是ByteBuffer,實際就是字節數組。我們首先需要 將ByteBuffer緩沖區中的數據報讀取出來,并將其解碼為Java對象;然后對Java對象根據某些規則做二次解碼,將其解碼為另一個POJO對 象。因為MessageToMessageDecoder在ByteToMessageDecoder之后,所以稱之為二次解碼器。

二次解碼器在實際的商業項目中非常有用,以HTTP+XML協議棧為例,第一次解碼往往是將字節數組解碼成HttpRequest對象,然后對 HttpRequest消息中的消息體字符串進行二次解碼,將XML格式的字符串解碼為POJO對象,這就用到了二次解碼器。類似這樣的場景還有很多,不 再一一枚舉。

事實上,做一個超級復雜的解碼器將多個解碼器組合成一個大而全的MessageToMessageDecoder解碼器似乎也能解決多次解碼的問 題,但是采用這種方式的代碼可維護性會非常差。例如,如果我們打算在HTTP+XML協議棧中增加一個打印碼流的功能,即首次解碼獲取 HttpRequest對象之后打印XML格式的碼流。如果采用多個解碼器組合,在中間插入一個打印消息體的Handler即可,不需要修改原有的代碼; 如果做一個大而全的解碼器,就需要在解碼的方法中增加打印碼流的代碼,可擴展性和可維護性都會變差。

用戶的解碼器只需要實現void decode(ChannelHandlerContext ctx, I msg, List<Object> out)抽象方法即可,由于它是將一個POJO解碼為另一個POJO,所以一般不會涉及到半包的處理,相對于ByteToMessageDecoder更 加簡單些。它的繼承關系圖如下所示:

Netty編解碼框架分析

圖3-2 MessageToMessageDecoder 解碼器繼承關系圖

3.2. 編碼器

3.2.1. MessageToByteEncoder抽象編碼器

MessageToByteEncoder負責將POJO對象編碼成ByteBuf,用戶的編碼器繼承Message ToByteEncoder,實現void encode(ChannelHandlerContext ctx, I msg, ByteBuf out)接口接口,示例代碼如下:

public class IntegerEncoder extends MessageToByteEncoder<Integer> {
      @Override
      public void encode(ChannelHandlerContext ctx, Integer msg,ByteBuf out)
         throws Exception {
             out.writeInt(msg);
          }
      }

它的實現原理如下:調用write操作時,首先判斷當前編碼器是否支持需要發送的消息,如果不支持則直接透傳;如果支持則判斷緩沖區的類型,對于直接內存分配ioBuffer(堆外內存),對于堆內存通過heapBuffer方法分配,源碼如下:

Netty編解碼框架分析

編碼使用的緩沖區分配完成之后,調用encode抽象方法進行編碼,方法定義如下:它由子類負責具體實現。

Netty編解碼框架分析

編碼完成之后,調用ReferenceCountUtil的release方法釋放編碼對象msg。對編碼后的ByteBuf進行以下判斷:

1) 如果緩沖區包含可發送的字節,則調用ChannelHandlerContext的write方法發送ByteBuf;

2) 如果緩沖區沒有包含可寫的字節,則需要釋放編碼后的ByteBuf,寫入一個空的ByteBuf到ChannelHandlerContext中。

發送操作完成之后,在方法退出之前釋放編碼緩沖區ByteBuf對象。

3.2.2. MessageToMessageEncoder抽象編碼器

將一個POJO對象編碼成另一個對象,以HTTP+XML協議為例,它的一種實現方式是:先將POJO對象編碼成XML字符串,再將字符串編碼為HTTP請求或者應答消息。對于復雜協議,往往需要經歷多次編碼,為了便于功能擴展,可以通過多個編碼器組合來實現相關功能。

用戶的解碼器繼承MessageToMessageEncoder解碼器,實現void encode(Channel HandlerContext ctx, I msg, List<Object> out)方法即可。注意,它與MessageToByteEncoder的區別是輸出是對象列表而不是ByteBuf,示例代碼如下:

public class IntegerToStringEncoder extends MessageToMessageEncoder <Integer> 
 {
          @Override
          public void encode(ChannelHandlerContext ctx, Integer message, 
             List<Object> out)
                  throws Exception 
          {
              out.add(message.toString());
          }
      }

MessageToMessageEncoder編碼器的實現原理與之前分析的MessageToByteEncoder相似,唯一的差別是它編碼后的輸出是個中間對象,并非最終可傳輸的ByteBuf。

簡單看下它的源碼實現:創建RecyclableArrayList對象,判斷當前需要編碼的對象是否是編碼器可處理的類型,如果不是,則忽略,執行下一個ChannelHandler的write方法。

具體的編碼方法實現由用戶子類編碼器負責完成,如果編碼后的RecyclableArrayList為空,說明編碼沒有成功,釋放RecyclableArrayList引用。

如果編碼成功,則通過遍歷RecyclableArrayList,循環發送編碼后的POJO對象,代碼如下所示:

Netty編解碼框架分析

3.2.3. LengthFieldPrepender編碼器

如果協議中的第一個字段為長度字段,Netty提供了LengthFieldPrepender編碼器,它可以計算當前待發送消息的二進制字節長度,將該長度添加到ByteBuf的緩沖區頭中,如圖所示:

Netty編解碼框架分析

圖3-3 LengthFieldPrepender編碼器

通過LengthFieldPrepender可以將待發送消息的長度寫入到ByteBuf的前2個字節,編碼后的消息組成為長度字段+原消息的方式。

通過設置LengthFieldPrepender為true,消息長度將包含長度本身占用的字節數,打開LengthFieldPrepender后,圖3-3示例中的編碼結果如下圖所示:

Netty編解碼框架分析

圖3-4 打開LengthFieldPrepender開關后編碼效果

LengthFieldPrepender工作原理分析如下:首先對長度字段進行設置,如果需要包含消息長度自身,則在原來長度的基礎之上再加上lengthFieldLength的長度。

如果調整后的消息長度小于0,則拋出參數非法異常。對消息長度自身所占的字節數進行判斷,以便采用正確的方法將長度字段寫入到ByteBuf中,共有以下6種可能:

1) 長度字段所占字節為1:如果使用1個Byte字節代表消息長度,則最大長度需要小于256個字節。對長度進行校驗,如果校驗失敗,則拋出參數非法異常;若校驗通過,則創建新的ByteBuf并通過writeByte將長度值寫入到ByteBuf中;

2) 長度字段所占字節為2:如果使用2個Byte字節代表消息長度,則最大長度需要小于65536個字節,對長度進行校驗,如果校驗失敗,則拋出參數非法異常;若校驗通過,則創建新的ByteBuf并通過writeShort將長度值寫入到ByteBuf中;

3) 長度字段所占字節為3:如果使用3個Byte字節代表消息長度,則最大長度需要小于16777216個字節,對長度進行校驗,如果校驗失敗,則拋出參數非法異常;若校驗通過,則創建新的ByteBuf并通過writeMedium將長度值寫入到ByteBuf中;

4) 長度字段所占字節為4:創建新的ByteBuf,并通過writeInt將長度值寫入到ByteBuf中;

5) 長度字段所占字節為8:創建新的ByteBuf,并通過writeLong將長度值寫入到ByteBuf中;

6) 其它長度值:直接拋出Error。

相關代碼如下:

Netty編解碼框架分析

最后將原需要發送的ByteBuf復制到List<Object> out中,完成編碼:

Netty編解碼框架分析

4. 作者簡介

李林鋒,2007年畢業于東北大學,2008年進入華為公司從事高性能通信軟件的設計和開發工作,有7年NIO設計和開發經驗,精通Netty、Mina等NIO框架和平臺中間件,現任華為軟件平臺架構部架構師,《Netty權威指南》作者。

聯系方式:新浪微博 Nettying 微信:Nettying 微信公眾號:Netty之家

對于Netty學習中遇到的問題,或者認為有價值的Netty或者NIO相關案例,可以通過上述幾種方式聯系我。

來自:http://www.infoq.com/cn/articles/netty-codec-framework-analyse

 本文由用戶 fpcm 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!