12.29 你來講下Netty通信中的粘包、拆包?

在進行Java NIO學習時,發現,如果客戶端連續不斷的向服務端發送數據包時,服務端接收的數據會出現兩個數據包粘在一起的情況,這就是TCP協議中經常會遇到的粘包以及拆包的問題。


你來講下Netty通信中的粘包、拆包?


一、什麼是粘包和拆包?

現在假設客戶端向服務端連續發送了兩個數據包,用packet1和packet2來表示,那麼服務端收到的數據可以分為三種,現列舉如下:

1.接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論範圍內。

你來講下Netty通信中的粘包、拆包?

2.接收端只收到一個數據包,由於TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即為粘包。這種情況由於接收端不知道這兩個數據包的界限,所以對於接收端來說很難處理。

你來講下Netty通信中的粘包、拆包?

3.這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要麼是不完整的,要麼就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。

你來講下Netty通信中的粘包、拆包?

你來講下Netty通信中的粘包、拆包?

TCP是個“流”協議,沒有界限的一串數據。TCP底層並不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。

二、粘包、拆包發生的原因

要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。

待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。

要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。

接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。

三、粘包、拆包解決辦法

由於底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,歸納如下:

消息定長。發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次接收緩衝區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。

設置消息邊界。服務端從網絡流中按消息邊界分離出消息內容。在包尾增加回車換行符進行分割,例如FTP協議。

將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度)的字段。

更復雜的應用層協議,比如Netty中實現的一些協議都對粘包、拆包做了很好的處理。


你來講下Netty通信中的粘包、拆包?


四、Netty的拆包解決方案

Netty這個框架,對於客戶端和服務端之間的數據傳輸做了很好的處理,服務端在發送數據之前先對數據按一定的規則進行編碼,客戶端在接收到數據後按照相同的規則進行解碼,這就是Netty解決粘包拆包問題的思路,下面我們詳細來看一看。

拆包這個工作,Netty 已經為大家備好了很多不同的拆包器。本著不重複發明輪子的原則,我們直接使用Netty現成的拆包器。

Netty 中的拆包器大致如下:

1.固定長度的拆包器 FixedLengthFrameDecoder

每個應用層數據包的都拆分成都是固定長度的大小,比如 1024字節。

對於使用固定長度的粘包和拆包場景,可以使用FixedLengthFrameDecoder,該解碼一器會每次讀取固定長度的消息,如果當前讀取到的消息不足指定長度,那麼就會等待下一個消息到達後進行補足。其使用也比較簡單,只需要在構造函數中指定每個消息的長度即可。這裡需要注意的是,FixedLengthFrameDecoder只是一個解碼一器,Netty也只提供了一個解碼一器,這是因為對於解碼是需要等待下一個包的進行補全的,代碼相對複雜,而對於編碼器,用戶可以自行編寫,因為編碼時只需要將不足指定長度的部分進行補全即可。

數據在編碼發送的時候,也會以固定長度作為一調完整的消息。

2.行拆包器 LineBasedFrameDecoder

每個應用層數據包,都以換行符作為分隔符,進行分割拆分。

數據在編碼發送的時候,會以換行符作為一條完整的消息。

3.分隔符拆包器 DelimiterBasedFrameDecoder

每個應用層數據包,都通過自定義的分隔符,進行分割拆分。這個版本,是LineBasedFrameDecoder 的通用版本,本質上是一樣的。

數據在編碼發送的時候,會以一個自定義的分隔符作為一條完整的消息。

4.基於數據包長度的拆包器 LengthFieldBasedFrameDecoder

將應用層數據包的長度,作為接收端應用層數據包的拆分依據。按照應用層數據包的大小,拆包。這個拆包器,有一個要求,就是應用層協議中包含數據包的長度。

LengthFieldBasedFrameDecoder與LengthFieldPrepender需要配合起來使用,其實本質上來講,這兩者一個是解碼,一個是編碼的關係。它們處理粘拆包的主要思想是在生成的數據包中添加一個長度字段,用於記錄當前數據包的長度。LengthFieldBasedFrameDecoder會按照參數指定的包長度偏移量數據對接收到的數據進行解碼,從而得到目標消息體數據;而LengthFieldPrepender則會在響應的數據前面添加指定的字節數據,這個字節數據中保存了當前消息體的整體字節數據長度。

數據在編碼發送的時候,會指定當前這條消息的長度。

五、實戰:遠揚通信中自定義協議的粘包、拆包解決方案

對於我們要做的項目,也可以自己定義消息傳輸的協議,在我做過的一個項目中,遠洋貨輪需要進行通信,大家都知道,在海上信號是很差的,每次收消息都很難保證收到的是一條完整的消息,但此時我們可以自定義協議,在消息的頭部用兩個字節把本次消息發送的長度加上,中間部分是消息正文,消息的尾部用四個字節保存本條消息md5值的低四位。這樣,接收方在收到部分消息後,可根據消息的頭部判斷該條消息的具體長度,然後繼續接收消息,當收到完整的消息後,在去計算接收到消息的md5四位,去跟接收的低四位做比較,如果一致,就認為是收到了完整的消息,接著根據約定的協議進行解碼交流。

當然,消息的校驗位還是比較複雜的,需要給每臺設備都指定唯一標識來區別身份等,這裡就不展開敘述了。

此種方案特別適合網絡環境差的情況,能保證正常的通信。也經受住了實戰的考驗,基本可以做到消息的零丟失。


你來講下Netty通信中的粘包、拆包?


六、總結,一個面試題

我們都知道TCP屬於傳輸層的協議,傳輸層除了有TCP協議外還有UDP協議。

那麼UDP是否會發生粘包或拆包的現象呢?

答案是不會。UDP是基於報文發送的,從UDP的幀結構可以看出,在UDP首部採用了16bit來指示UDP數據報文的長度,因此在應用層能很好的將不同的數據報文區分開,從而避免粘包和拆包的問題。

而TCP是基於字節流的,雖然應用層和TCP傳輸層之間的數據交互是大小不等的數據塊,但是TCP把這些數據塊僅僅看成一連串無結構的字節流,沒有邊界;另外從TCP的幀結構也可以看出,在TCP的首部沒有表示數據長度的字段,基於上面兩點,在使用TCP傳輸數據時,才有粘包或者拆包現象發生的可能。


分享到:


相關文章: