TCP 粘包和半包 介紹及解決(上)
在網(wǎng)絡(luò)傳輸中,粘包和半包應(yīng)該是最常出現(xiàn)的問題,作為 Java 中最常使用的 NIO 網(wǎng)絡(luò)框架 Netty,它又是如何解決的呢?今天就讓我們來看看。
定義
TCP 傳輸中,客戶端發(fā)送數(shù)據(jù),實際是把數(shù)據(jù)寫入到了 TCP 的緩存中,粘包和半包也就會在此時產(chǎn)生。
客戶端給服務(wù)端發(fā)送了兩條消息ABC和DEF,服務(wù)端這邊的接收會有多少種情況呢?有可能是一次性收到了所有的消息ABCDEF,有可能是收到了三條消息AB、CD、EF。
上面所說的一次性收到了所有的消息ABCDEF,類似于粘包。如果客戶端發(fā)送的包的大小比 TCP 的緩存容量小,并且 TCP 緩存可以存放多個包,那么客戶端和服務(wù)端的一次通信就可能傳遞了多個包,這時候服務(wù)端從 TCP 緩存就可能一下讀取了多個包,這種現(xiàn)象就叫粘包。
上面說的后面那種收到了三條消息AB、CD、EF,類似于半包。如果客戶端發(fā)送的包的大小比 TCP 的緩存容量大,那么這個數(shù)據(jù)包就會被分成多個包,通過 Socket 多次發(fā)送到服務(wù)端,服務(wù)端第一次從接受緩存里面獲取的數(shù)據(jù),實際是整個包的一部分,這時候就產(chǎn)生了半包(半包不是說只收到了全包的一半,是說收到了全包的一部分)。
產(chǎn)生原因
其實從上面的定義,我們就可以大概知道產(chǎn)生的原因了。
粘包的主要原因:
- 發(fā)送方每次寫入數(shù)據(jù) < 套接字(Socket)緩沖區(qū)大小
- 接收方讀取套接字(Socket)緩沖區(qū)數(shù)據(jù)不夠及時
半包的主要原因:
- 發(fā)送方每次寫入數(shù)據(jù) > 套接字(Socket)緩沖區(qū)大小
- 發(fā)送的數(shù)據(jù)大于協(xié)議的 MTU (Maximum Transmission Unit,最大傳輸單元),因此必須拆包
其實我們可以換個角度看待問題:
- 從收發(fā)的角度看,便是一個發(fā)送可能被多次接收,多個發(fā)送可能被一次接收。
- 從傳輸?shù)慕嵌瓤?,便是一個發(fā)送可能占用多個傳輸包,多個發(fā)送可能共用一個傳輸包。
根本原因,其實是
TCP 是流式協(xié)議,消息無邊界。
(PS : UDP 雖然也可以一次傳輸多個包或者多次傳輸一個包,但每個消息都是有邊界的,因此不會有粘包和半包問題。)
解決方法
就像上面說的,UDP 之所以不會產(chǎn)生粘包和半包問題,主要是因為消息有邊界,因此,我們也可以采取類似的思路。
1. 改成短連接
將 TCP 連接改成短連接,一個請求一個短連接。這樣的話,建立連接到釋放連接之間的消息即為傳輸?shù)男畔?,消息也就產(chǎn)生了邊界。
這樣的方法就是十分簡單,不需要在我們的應(yīng)用中做過多修改。但缺點也就很明顯了,效率低下,TCP 連接和斷開都會涉及三次握手以及四次握手,每個消息都會涉及這些過程,十分浪費性能。
因此,并不推介這種方式。
2. 封裝成幀
封裝成幀(Framing),也就是原本發(fā)送消息的單位是緩沖大小,現(xiàn)在換成了幀,這樣我們就可以自定義邊界了。一般有4種方式:
3. 固定長度
這種方式下,消息邊界也就是固定長度即可。
優(yōu)點就是實現(xiàn)很簡單,缺點就是空間有極大的浪費,如果傳遞的消息中大部分都比較短,這樣就會有很多空間是浪費的。
因此,這種方式一般也是不推介的。
4. 分隔符
這種方式下,消息邊界也就是分隔符本身。
優(yōu)點是空間不再浪費,實現(xiàn)也比較簡單。缺點是當內(nèi)容本身出現(xiàn)分割符時需要轉(zhuǎn)義,所以無論是發(fā)送還是接受,都需要進行整個內(nèi)容的掃描。
因此,這種方式效率也不是很高,但可以嘗試使用。
5. 專門的 length 字段
這種方式,就有點類似 Http 請求中的 Content-Length,有一個專門的字段存儲消息的長度。作為服務(wù)端,接受消息時,先解析固定長度的字段(length字段)獲取消息總長度,然后讀取后續(xù)內(nèi)容。
優(yōu)點是精確定位用戶數(shù)據(jù),內(nèi)容也不用轉(zhuǎn)義。缺點是長度理論上有限制,需要提前限制可能的最大長度從而定義長度占用字節(jié)數(shù)。
因此,十分推介用這種方式。
6. 其他方式
其他方式就各不相同了,比如 JSON 可以看成是使用{}是否成對。這些優(yōu)缺點就需要大家在各自的場景中進行衡量了。
Netty 中的實現(xiàn)
Netty 支持上文所講的封裝成幀(Framing)中的前三種方式,簡單介紹下: