1. 程式人生 > >netty]--最通用TCP黏包解決方案

netty]--最通用TCP黏包解決方案

netty]--最通用TCP黏包解決方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender

2017年02月19日 15:02:11  閱讀數:14555  

前面已經說過: 
TCP以流的方式進行資料傳輸,上層應用協議為了對訊息進行區分,往往採用如下4種方式。 
(1)訊息長度固定:累計讀取到固定長度為LENGTH之後就認為讀取到了一個完整的訊息。然後將計數器復位,重新開始讀下一個資料報文。

(2)回車換行符作為訊息結束符:在文字協議中應用比較廣泛。

(3)將特殊的分隔符作為訊息的結束標誌,回車換行符就是一種特殊的結束分隔符。

(4)通過在訊息頭中定義長度欄位來標示訊息的總長度。

netty中針對這四種場景均有對應的解碼器作為解決方案,比如:

(1)通過FixedLengthFrameDecoder 定長解碼器來解決定長訊息的黏包問題;

(2)通過LineBasedFrameDecoder和StringDecoder來解決以回車換行符作為訊息結束符的TCP黏包的問題;

(3)通過DelimiterBasedFrameDecoder 特殊分隔符解碼器來解決以特殊符號作為訊息結束符

的TCP黏包問題;

(4)最後一種,也是本文的重點,通過LengthFieldBasedFrameDecoder 自定義長度解碼器解決TCP黏包問題。

大多數的協議在協議頭中都會攜帶長度欄位,用於標識訊息體或則整包訊息的長度。LengthFieldBasedFrameDecoder通過指定長度來標識整包訊息,這樣就可以自動的處理黏包和半包訊息,只要傳入正確的引數,就可以輕鬆解決“讀半包”的問題。

1. LengthFieldBasedFrameDecoder功能說明

下面我們通過例項來看如何通過配置不同的引數組合來實現不同的半包讀取策略。

(1)場景一:訊息的第一個欄位是長度欄位,後面是訊息體,訊息頭中只包含一個長度欄位,訊息結構定義如下: 
訊息結構圖

在解碼前位元組緩衝區佔了14個位元組,其中前兩個位元組是標識長度的位元組,後面12個位元組是訊息體。 
使用的組合引數如下: 
1) lengthFieldOffset = 0;//長度欄位的偏差

2) lengthFieldLength = 2;//長度欄位佔的位元組數

3) lengthAdjustment = 0;//新增到長度欄位的補償值

4) initialBytesToStrip = 0。//從解碼幀中第一次去除的位元組數

解碼後的位元組緩衝區的內容是: 
解碼後

解碼後還是14個位元組。

(2)場景二:通過ByteBuf.readableBytes()方法我們可以獲取當前訊息的長度,所以解碼後的位元組緩衝區可以不攜帶長度欄位,由於長度欄位在起始位置並且長度為2,所以將initialBytesToStrip設定為2。引數組合修改為: 
1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 2。

這時候解碼後位元組緩衝區的資料就是: 
這裡寫圖片描述

很明顯跳過了長度欄位後的位元組緩衝區就只有12個位元組了。

解碼後的位元組緩衝區丟棄了長度欄位,僅僅包含訊息體,對於大多數的協議,解碼之後訊息長度沒有用處,因此可以丟棄。

(3)場景三:在大多數的應用場景中,長度欄位僅用來標識訊息體的長度,這類協議通常由訊息長度欄位+訊息體組成,如上圖所示的幾個例子。但是,對於某些協議,長度欄位還包含了訊息頭的長度。在這種應用場景中,往往需要使用lengthAdjustment進行修正。由於整個訊息(包含訊息頭)的長度往往大於訊息體的長度,所以,lengthAdjustment為負數。下圖展示了通過指定lengthAdjustment欄位來包含訊息頭的長度: 
1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = -2;

4) initialBytesToStrip = 0。

解碼之前的碼流: 
這裡寫圖片描述 
包含欄位長度的碼流:

解碼只有的碼流是: 
這裡寫圖片描述

(4)場景四:但是由於協議的種類繁多,並不是所有的協議都將長度欄位放在訊息頭的首位,當標識訊息長度的欄位位於訊息頭的中間或者尾部時,需要使用lengthFieldOffset欄位進行標識,下面的引數組合給出瞭如何解決訊息長度欄位不在首位的問題: 
1) lengthFieldOffset = 2;

2) lengthFieldLength = 3;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 0。

其中lengthFieldOffset表示長度欄位在訊息頭中偏移的位元組數,lengthFieldLength 表示長度欄位自身的長度,解碼效果如下: 
解碼之前: 
這裡寫圖片描述

解碼之後: 
這裡寫圖片描述

由於訊息頭1的長度為2,所以長度欄位的偏移量為2;訊息長度欄位Length為3,所以lengthFieldLength值為3。由於長度欄位僅僅標識訊息體的長度,所以lengthAdjustment和initialBytesToStrip都為0。

(5)場景五:最後一種場景是長度欄位夾在兩個訊息頭之間或者長度欄位位於訊息頭的中間,前後都有其它訊息頭欄位,在這種場景下如果想忽略長度欄位以及其前面的其它訊息頭欄位,則可以通過initialBytesToStrip引數來跳過要忽略的位元組長度,它的組合配置示意如下: 
1) lengthFieldOffset = 1;

2) lengthFieldLength = 2;

3) lengthAdjustment = 1;

4) initialBytesToStrip = 3。

解碼之前16位元組: 
這裡寫圖片描述

解碼之後13位元組: 
這裡寫圖片描述

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

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

 

https://blog.csdn.net/a925907195/article/details/74942472  

粘包問題的解決策略

由於底層的TCP無法理解上層的業務資料,所以在底層是無法保證資料包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決。業界的主流協議的解決方案,可以歸納如下: 
1. 訊息定長,報文大小固定長度,例如每個報文的長度固定為200位元組,如果不夠空位補空格; 
2. 包尾新增特殊分隔符,例如每條報文結束都添加回車換行符(例如FTP協議)或者指定特殊字元作為報文分隔符,接收方通過特殊分隔符切分報文區分; 
3. 將訊息分為訊息頭和訊息體,訊息頭中包含表示資訊的總長度(或者訊息體長度)的欄位; 
4. 更復雜的自定義應用層協議。

Netty提供了多個解碼器,可以進行分包的操作,分別是: 
* LineBasedFrameDecoder 
* DelimiterBasedFrameDecoder(新增特殊分隔符報文來分包) 
* FixedLengthFrameDecoder(使用定長的報文來分包) 
* LengthFieldBasedFrameDecoder

LineBasedFrameDecoder解碼器

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