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