李林鋒 IT哈哈 java
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倍,效率很是差。
圖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源於Facebook,在2007年Facebook將Thrift做爲一個開源項目提交給Apache基金會。對於當時的 Facebook來講,創造Thrift是爲了解決Facebook各系統間大數據量的傳輸通訊以及系統之間語言環境不一樣須要跨平臺的特性,所以 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.1. Netty爲何要提供編解碼框架
做爲一個高性能的異步、NIO通訊框架,編解碼框架是Netty的重要組成部分。儘管站在微內核的角度看,編解碼框架並非Netty微內核的組成部分,可是經過ChannelHandler定製擴展出的編解碼框架倒是不可或缺的。
下面咱們從幾個角度詳細談下這個話題,首先一塊兒看下Netty的邏輯架構圖:
圖2-1 Netty邏輯架構圖
從網絡讀取的inbound消息,須要通過解碼,將二進制的數據報轉換成應用層協議消息或者業務消息,纔可以被上層的應用邏輯識別和處理;同理,用 戶發送到網絡的outbound業務消息,須要通過編碼轉換成二進制字節數組(對於Netty就是ByteBuf)纔可以發送到網絡對端。編碼和解碼功能 是NIO框架的有機組成部分,不管是由業務定製擴展實現,仍是NIO框架內置編解碼能力,該功能是必不可少的。
爲了下降用戶的開發難度,Netty對經常使用的功能和API作了裝飾,以屏蔽底層的實現細節。編解碼功能的定製,對於熟悉Netty底層實現的開發者 而言,直接基於ChannelHandler擴展開發,難度並非很大。可是對於大多數初學者或者不肯意去了解底層實現細節的用戶,須要提供給他們更簡單 的類庫和API,而不是ChannelHandler。
Netty在這方面作得很是出色,針對編解碼功能,它既提供了通用的編解碼框架供用戶擴展,又提供了經常使用的編解碼類庫供用戶直接使用。在保證定製擴展性的基礎之上,儘可能下降用戶的開發工做量和開發門檻,提高開發效率。
Netty預置的編解碼功能列表以下:base6四、Protobuf、JBoss Marshalling、spdy等。
圖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返回,源碼以下:
詳細分析下indexOf(buffer, delim)方法的實現,代碼以下:
該算法與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底層的拆包和粘 包問題,只須要傳入正確的參數,便可輕鬆解決「讀半包「問題。
下面咱們看看如何經過參數組合的不一樣來實現不一樣的「半包」讀取策略。第一種經常使用的方式是消息的第一個字段是長度字段,後面是消息體,消息頭中只包含一個長度字段。它的消息結構定義如圖所示:
圖2-3 解碼前的字節緩衝區(14字節)
使用如下參數組合進行解碼:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 0。
解碼後的字節緩衝區內容如圖所示:
圖2-4 解碼後的字節緩衝區(14字節)
經過ByteBuf.readableBytes()方法咱們能夠獲取當前消息的長度,因此解碼後的字節緩衝區能夠不攜帶長度字段,因爲長度字段在起始位置而且長度爲2,因此將initialBytesToStrip設置爲2,參數組合修改成:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 2。
解碼後的字節緩衝區內容如圖所示:
圖2-5 跳過長度字段解碼後的字節緩衝區(12字節)
解碼後的字節緩衝區丟棄了長度字段,僅僅包含消息體,對於大多數的協議,解碼以後消息長度沒有用處,所以能夠丟棄。
在大多數的應用場景中,長度字段僅用來標識消息體的長度,這類協議一般由消息長度字段+消息體組成,如上圖所示的幾個例子。可是,對於某些協議,長 度字段還包含了消息頭的長度。在這種應用場景中,每每須要使用lengthAdjustment進行修正。因爲整個消息(包含消息頭)的長度每每大於消息 體的長度,因此,lengthAdjustment爲負數。圖2-6展現了經過指定lengthAdjustment字段來包含消息頭的長度:
1) lengthFieldOffset = 0;
2) lengthFieldLength = 2;
3) lengthAdjustment = -2;
4) initialBytesToStrip = 0。
解碼以前的碼流:
圖2-6 包含長度字段自身的碼流
解碼以後的碼流:
圖2-7 解碼後的碼流
因爲協議種類繁多,並非全部的協議都將長度字段放在消息頭的首位,當標識消息長度的字段位於消息頭的中間或者尾部時,須要使用lengthFieldOffset字段進行標識,下面的參數組合給出瞭如何解決消息長度字段不在首位的問題:
1) lengthFieldOffset = 2;
2) lengthFieldLength = 3;
3) lengthAdjustment = 0;
4) initialBytesToStrip = 0。
其中lengthFieldOffset表示長度字段在消息頭中偏移的字節數,lengthFieldLength 表示長度字段自身的長度,解碼效果以下:
解碼以前:
圖2-8 長度字段偏移的原始碼流
解碼以後:
圖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字節):
圖2-10長度字段夾在消息頭中間的原始碼流(16字節)
解碼以後的碼流(13字節):
圖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:
若是要使用Java序列化,對象必須實現Serializable接口,所以,它的泛型類型爲Serializable。
MessageToByteEncoder的子類只須要實現encode(ChannelHandlerContext ctx, I msg, ByteBuf out)方法便可,下面咱們重點關注encode方法的實現:
首先建立ByteBufOutputStream和ObjectOutputStream,用於將Object對象序列化到ByteBuf中,值得注意的是在writeObject以前須要先將長度字段(4個字節)預留,用於後續長度字段的更新。
依次寫入長度佔位符(4字節)、序列化以後的Object對象,以後根據ByteBuf的writeIndex計算序列化以後的碼流長度,最後調用ByteBuf的setInt(int index, int value)更新長度佔位符爲實際的碼流長度。
有個細節須要注意,更新碼流長度字段使用了setInt方法而不是writeInt,緣由就是setInt方法只更新內容,並不修改readerIndex和writerIndex。
儘管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也是不少基礎解碼器的父類,它的繼承關係以下圖所示:
圖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更 加簡單些。它的繼承關係圖以下所示:
圖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方法分配,源碼以下:
編碼使用的緩衝區分配完成以後,調用encode抽象方法進行編碼,方法定義以下:它由子類負責具體實現。
編碼完成以後,調用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對象,代碼以下所示:
3.2.3. LengthFieldPrepender編碼器
若是協議中的第一個字段爲長度字段,Netty提供了LengthFieldPrepender編碼器,它能夠計算當前待發送消息的二進制字節長度,將該長度添加到ByteBuf的緩衝區頭中,如圖所示:
圖3-3 LengthFieldPrepender編碼器
經過LengthFieldPrepender能夠將待發送消息的長度寫入到ByteBuf的前2個字節,編碼後的消息組成爲長度字段+原消息的方式。
經過設置LengthFieldPrepender爲true,消息長度將包含長度自己佔用的字節數,打開LengthFieldPrepender後,圖3-3示例中的編碼結果以下圖所示:
圖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。
相關代碼以下:
最後將原須要發送的ByteBuf複製到List<Object> out中,完成編碼: