你真的理解粘包與半包嗎?三分鐘搞懂它
通俗的例子
這里先舉個可能不太恰當,但是很容易理解的例子。
比如,平時我們要寄快遞,如果東西太大的話,那么就需要拆成幾個包裹來郵寄。
收件人僅收到個別包裹的時候,東西是不完整的,對應到網絡傳輸中,這種情況就叫半包。
只有等接收到全部包裹時,這個東西(傳輸的信息)才完整,所以半包情況下無法解析出完整的數據,需要等,等接收到全部包裹。
那么問題來了,如何知曉已經收到全部包裹了呢?下文我們再作分析。
再比如,快過年了,我打算給家里的親戚送點禮物,給每位長輩送個手表,我們都知道手表的體積不大,并且我家里人都住在一個村,所以把給各長輩的禮物打包在一個包裹里郵寄,這樣能節(jié)省運費。
這種把本應該分多個包傳輸的數據合成一個包發(fā)送的情況,對應到網絡傳輸中,就叫粘包。
看完這個例子之后,應該對粘包與半包有點感覺了,接下來我們看下網絡中實際的情況。
實際情況
粘包與半包只有在 TCP 傳輸的時候才會有,像 UDP 是不會有這種情況的,原因是因為 TCP 是面向流的,數據之間沒有界限的,而 UDP 是有的界限的。
如果熟悉 TCP 和 UDP 報文格式的同學肯定知道,TCP 的包沒有報文長度,而 UDP 的包有報文長度,這也說明了 TCP 為什么是流式。
所以我為什么說上面的例子不太恰當,因為現實生活中快遞的包裹之間其實是有界限的,TCP 則像流水,沒有明確的界限。
然后 TCP 有發(fā)送緩沖區(qū)的概念,UDP 實際上是沒這個概念。
假設 TCP 一次傳輸的數據大小超過發(fā)送緩沖區(qū)大小,那么一個完整的報文就需要被拆分成兩個或更多的小報文,這可能會產生半包的情況,當接收端收到不完整的數據,是無法解析成功的。
如果 TCP 一次傳輸的數據大小小于發(fā)送緩沖區(qū),那么可能會跟別的報文合并起來一塊發(fā)送,這就是粘包。
此時接收端也無法正常解析報文,需要將其拆成多個正確的報文,才能正常解析。
關于粘包與半包,我還看到有拿 MTU (最大傳輸單元)說事的,如果發(fā)送的數據大于 MTU 那就會出現拆包,導致半包的情況。
我個人覺得這里有點不對,簡單理解下,UDP 也是要遵循 MTU 的呀,對吧?那它咋不會發(fā)生半包呢?
我們接著來看如何解決粘包與半包。
那如何解決粘包與半包問題呢?
- 粘包:這個思路其實很清晰,就是把它拆開唄,具體就是看怎么拆了,比如我們可以固定長度,我們規(guī)定每個包都是10個字節(jié),那么就10個字節(jié)切一刀,這樣拆開解析就 ok 了。
- 半包:半包其實就是信息還不完整,我們需要等接收到全部的信息之后再作處理,當我們識別這是一個不完整的包時候,我們先 hold 住,不作處理,等待數據完整再處理。這里關鍵點在于,我們如何才能知道此時完整了?上面說的固定長度其實也是一點,當然還有更多更好的解決方案,我們接著往下看。
實際常見解決粘包與半包問題有三個方案:
- 固定長度
- 分隔符
- 固定長度字段+內容
為了說明方便,以下沒有按二進制的位等單位來描述。
固定長度
這個其實很簡單,比如現在要傳輸 ABC、EF 這兩個包,如果不做處理接收端很可能收到的是 AB、CEF 或者 ABCE、F 等等。
這時候我們固定長度,我們規(guī)定每個報文長度都是 3,如果一個報文實際數據不足 3,那么就用空字符填充一下 。
所以我們發(fā)送的報文是 :
接收到的情況可能是:
但我們是按照 3 位來處理的,所以一次只會按照 3 位來解析,所以第一次雖然收到的數據是 ABCE,但我們就解析 3 位,即解析出 ABC,留著了個 E,等我們要繼續(xù)解析 3 位的時候,發(fā)現長度不足 3,所以我們暫時先不管,先等等。
后面等到了 F“”,我們發(fā)現當下數據又滿足 3 位了,所以我們接著解析 EF“” 。
這樣就解決了粘包與半包問題。
對應到 Netty 中的實現就是 FixedLengthFrameDecoder,這個類來實現固定長度的解碼。
核心邏輯就是我上面說的,我們來看下源碼,很簡單:
固定長度的優(yōu)點:簡單。
缺點:固定長度很僵硬,不易于擴展,且如果設置過大來滿足業(yè)務場景的話,會導致空間浪費,因為不足長度的需要填充。
分隔符
這個應該很好理解, 還是拿 ABC、EF 這兩個包舉例,我在寫完 ABC后,插入一個分號,組成ABC;,EF 同理:
這樣以分隔符為界限來切分無界限的 TCP 流,來解決粘包與半包問題,這個應該很好理解,既然你 TCP 沒界限,我業(yè)務上給你搞個界限。
對應到 Netty 中的實現就是 DelimiterBasedFrameDecoder,具體源碼就不貼了,有點長,不過道理還是簡單的。
一直解析,等識別到分隔符之后,說明前面的數據完整了,于是解析前面的數據,然后繼續(xù)往后掃描解析。
分隔符的優(yōu)點:簡單,也不會浪費空間。
缺點:需要對內容本身進行處理,防止內容內出現分隔符,這樣就會導致錯亂,所以需要掃描一遍傳輸的數據將其轉義,或者可以用 base64 編碼數據,用 64 個之外的字符作為分隔符即可。
分隔符的處理方式在業(yè)界也是常用的,比如 Redis 就用換行符來分隔。
固定長度字段+內容
這個也很好理解,比如協議規(guī)定固定 4 位存放內容的長度,這樣內容就可以伸縮:
還是拿 ABC、EF 這兩個包舉例:
解析流程是:先獲取 4 位,如果當前收到的數據不夠 4 位,那就再等等,夠 4 位之后解析得到長度是 3,所以我再往后取 3 位,同樣數據如果不夠 3 位就再等等,夠了的話就解析,這樣就獲取一個完整的包了。
然后接著往后獲取 4 位,解析得到 2,同理根據 2 往后再取 2 位,解析得到 EF。
這種方式就是先解析固定長度的字段,獲得后面內容的長度,根據內容長度來獲取內容,從而得到一個完整的報文。
對應到 Netty 中的實現就是 LengthFieldBasedFrameDecoder,具體源碼就不貼了,有點長,
固定長度字段+內容的優(yōu)點:可以根據固定字段精準定位,也不用掃描轉義字符。
缺點:固定長度字段的設計比較困難,大了浪費空間,畢竟每個報文都帶這個長度,小了可能不夠用。
總結
好了,我們總結一下。
因為 TCP 是面向流的協議,且利用緩沖區(qū)來提高發(fā)送的效率,所以會導致粘包/半包情況的發(fā)生。
對于這種情況,我們可以在報文上動手腳,可以約定固定長度的報文,或埋入分隔符,或利用固定長度字段+內容等常見的三種方式來解決粘包、半包的問題。
以上三種在 Netty 中都有現成實現類,可直接使用:
FixedLengthFrameDecoder,固定長度
DelimiterBasedFrameDecoder,分隔符
LengthFieldBasedFrameDecoder,定長度字段+內容
建議實驗一下,會有更清晰的認識。