1. 程式人生 > >Netty - 粘包和半包(上)

Netty - 粘包和半包(上)

在網路傳輸中,粘包和半包應該是最常出現的問題,作為 Java 中最常使用的 NIO 網路框架 Netty,它又是如何解決的呢?今天就讓我們來看看。

定義

TCP 傳輸中,客戶端傳送資料,實際是把資料寫入到了 TCP 的快取中,粘包和半包也就會在此時產生。

客戶端給服務端傳送了兩條訊息ABCDEF,服務端這邊的接收會有多少種情況呢?有可能是一次性收到了所有的訊息ABCDEF,有可能是收到了三條訊息ABCDEF

上面所說的一次性收到了所有的訊息ABCDEF,類似於粘包。如果客戶端傳送的包的大小比 TCP 的快取容量小,並且 TCP 快取可以存放多個包,那麼客戶端和服務端的一次通訊就可能傳遞了多個包,這時候服務端從 TCP 快取就可能一下讀取了多個包,這種現象就叫粘包

上面說的後面那種收到了三條訊息ABCDEF,類似於半包。如果客戶端傳送的包的大小比 TCP 的快取容量大,那麼這個資料包就會被分成多個包,通過 Socket 多次傳送到服務端,服務端第一次從接受快取裡面獲取的資料,實際是整個包的一部分,這時候就產生了半包(半包不是說只收到了全包的一半,是說收到了全包的一部分)。

產生原因

其實從上面的定義,我們就可以大概知道產生的原因了。

粘包的主要原因:

  1. 傳送方每次寫入資料 < 套接字(Socket)緩衝區大小
  2. 接收方讀取套接字(Socket)緩衝區資料不夠及時

半包的主要原因:

  1. 傳送方每次寫入資料 > 套接字(Socket)緩衝區大小
  2. 傳送的資料大於協議的 MTU (Maximum Transmission Unit,最大傳輸單元),因此必須拆包

其實我們可以換個角度看待問題:

  1. 收發的角度看,便是一個傳送可能被多次接收,多個傳送可能被一次接收。
  2. 傳輸的角度看,便是一個傳送可能佔用多個傳輸包,多個傳送可能共用一個傳輸包。

根本原因,其實是

TCP 是流式協議,訊息無邊界。

(PS : UDP 雖然也可以一次傳輸多個包或者多次傳輸一個包,但每個訊息都是有邊界的,因此不會有粘包和半包問題。)

解決方法

就像上面說的,UDP 之所以不會產生粘包和半包問題,主要是因為訊息有邊界,因此,我們也可以採取類似的思路。

改成短連線

將 TCP 連線改成短連線,一個請求一個短連線。這樣的話,建立連線到釋放連線之間的訊息即為傳輸的資訊,訊息也就產生了邊界。

這樣的方法就是十分簡單,不需要在我們的應用中做過多修改。但缺點也就很明顯了,效率低下,TCP 連線和斷開都會涉及三次握手以及四次握手,每個訊息都會涉及這些過程,十分浪費效能。

因此,並不推介這種方式。

封裝成幀

封裝成幀(Framing),也就是原本傳送訊息的單位是緩衝大小,現在換成了幀,這樣我們就可以自定義邊界了。一般有4種方式:

固定長度

這種方式下,訊息邊界也就是固定長度即可。

優點就是實現很簡單,缺點就是空間有極大的浪費,如果傳遞的訊息中大部分都比較短,這樣就會有很多空間是浪費的。

因此,這種方式一般也是不推介的。

分隔符

這種方式下,訊息邊界也就是分隔符本身。

優點是空間不再浪費,實現也比較簡單。缺點是當內容本身出現分割符時需要轉義,所以無論是傳送還是接受,都需要進行整個內容的掃描。

因此,這種方式效率也不是很高,但可以嘗試使用。

專門的 length 欄位

這種方式,就有點類似 Http 請求中的 Content-Length,有一個專門的欄位儲存訊息的長度。作為服務端,接受訊息時,先解析固定長度的欄位(length欄位)獲取訊息總長度,然後讀取後續內容。

優點是精確定位使用者資料,內容也不用轉義。缺點是長度理論上有限制,需要提前限制可能的最大長度從而定義長度佔用位元組數。

因此,十分推介用這種方式。

其他方式

其他方式就各不相同了,比如 JSON 可以看成是使用{}是否成對。這些優缺點就需要大家在各自的場景中進行衡量了。

Netty 中的實現

Netty 支援上文所講的封裝成幀(Framing)中的前三種方式,簡單介紹下:

方式 解碼 編碼
固定長度 FixedLengthFrameDecoder 簡單
分割符 DelimiterBasedFrameDecoder 簡單
專門的 length 欄位 LengthFieldBasedFrameDecoder LengthFieldPrepender

總結

今天主要介紹了粘包和半包問題、解決思路和 Netty 中的支援,我會在下一篇文章裡重點講述 Netty 中的具體實現,敬請期待。

有興趣的話可以訪問我的部落格或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/