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解碼成整包消息,后面的用戶解碼器拿到的就是個完整的數據報,按照邏輯正常進行解碼即可,不再需要額外考慮“讀半包”問題,降低了用戶的開發難度。
原文: http://www.infoq.com/cn/articles/netty-codec-framework-analyse/