硬核 - 圖解 TCP 粘包原理

事情從一個健身教練說起吧。李東,自稱亞健康終結者,嘗試使用互聯網 + 的模式拓展自己的業務。在某款新開發的聊天軟件琛琛上發佈廣告。鍵盤說來就來。瘋狂發送 "李東",回車發送!,"亞健康終結者",再回車發送!

還記得四層網絡協議長什麼樣子嗎?

四層網絡協議

四層網絡模型每層各司其職,消息在進入每一層時都會多加一個報頭,每多一個報頭可以理解爲數據報多戴一頂帽子。這個報頭上面記錄着消息從哪來,到哪去,以及消息多長等信息。比如,**mac頭部記錄的是硬件的唯一地址,IP頭記錄的是從哪來和到哪去,傳輸層頭記錄到是到達目的主機後具體去哪個進程 **。

在從消息發到網絡的時候給消息帶上報頭,消息和紛繁複雜的網絡中通過這些信息在路由器間流轉,最後到達目的機器上,接受者再通過這些報頭,一步一步還原出發送者最原始要發送的消息。

四層網絡協議 (1)

爲什麼要將數據切片

軟件琛琛是屬於應用層上的。

而 "李東","亞健康終結者" 這兩條消息在進入傳輸層時使用的是傳輸層上的 TCP 協議。消息在進入**傳輸層(TCP)**時會被切片爲一個個數據包。這個數據包的長度是MSS

可以把網絡比喻爲一個水管,是有一定的粗細的,這個粗細由網絡接口層(數據鏈路層)提供給網絡層,一般認爲是的MTU(1500),直接傳入整個消息,會超過水管的最大承受範圍,那麼,就需要進行切片,成爲一個個數據包,這樣消息才能正常通過 “水管”。

數據分片

MTU 和 MSS 有什麼區別

MSS 和 MTU 的區別

MTU: Maximum Transmit Unit,最大傳輸單元。 由網絡接口層(數據鏈路層)提供給網絡層最大一次傳輸數據的大小;一般 MTU=1500 Byte
假設 IP 層有 <= 1500 byte 需要發送,只需要一個 IP 包就可以完成發送任務;假設 IP 層有> 1500 byte 數據需要發送,需要分片才能完成發送,分片後的 IP Header ID 相同。•MSS:Maximum Segment Size 。TCP 提交給 IP 層最大分段大小,不包含 TCP Header 和  TCP Option,只包含 TCP Payload ,MSS 是 TCP 用來限制應用層最大的發送字節數。
假設 MTU= 1500 byte,那麼 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte,如果應用層有 2000 byte 發送,那麼需要兩個切片纔可以完成發送,第一個 TCP 切片 = 1460,第二個 TCP 切片 = 540。

什麼是粘包

那麼當李東在手機上鍵入 "李東"" 亞健康終結者 " 的時候,在 TCP 中把消息分成 MSS 大小後,消息順着網線順利發出。

發送消息到網絡

網絡穩得很,將消息分片傳到了對端手機 B 上。經過 TCP 層消息重組。變成 "李東亞健康終結者" 這樣的字節流(stream)

消息從網絡接收

但由於聊天軟件琛琛是新開發的,而且開發者叫小白,完了,是個臭名昭著的造 bug 工程師。經過他的代碼,在處理字節流的時候消息從 "李東","亞健康終結者" 變成了 "李東亞","健康終結者"。"李東" 作爲上一個包的內容與下一個包裏的 "亞" 粘在了一起被錯誤地當成了一個數據包解析了出來。這就是所謂的粘包

消息對比

一個號稱健康終結者的健身教練,大概運氣也不會很差吧,就祝他客源滾滾吧。

爲什麼會出現粘包

那就要從 TCP 是啥說起。

TCP,Transmission Control Protocol。傳輸控制協議,是一種面向連接的、可靠的、基於字節流的傳輸層通信協議。

TCP 是什麼

其中跟粘包關係最大的就是基於字節流這個特點。

字節流可以理解爲一個雙向的通道里流淌的數據,這個數據其實就是我們常說的二進制數據,簡單來說就是一大堆 01 串。這些 01 串之間沒有任何邊界

二進制字節流

應用層傳到 TCP 協議的數據,不是以消息報爲單位向目的主機發送,而是以字節流的方式發送到下游,這些數據可能被切割和組裝成各種數據包,接收端收到這些數據包後沒有正確還原原來的消息,因此出現粘包現象。

爲什麼要組裝發送的數據

上面提到 TCP 切割數據包是爲了能順利通過網絡這根水管。相反,還有一個組裝的情況。如果前後兩次 TCP 發的數據都遠小於 MSS,比如就幾個字節,每次都單獨發送這幾個字節,就比較浪費網絡 io 。

正常發送數據包

比如小白爸讓小白出門給買一瓶醬油,小白出去買醬油回來了。小白媽又讓小白出門買一瓶醋回來。小白前後結結實實跑了兩趟,影響了打遊戲的時間。

優化的方法也比較簡單。當小白爸讓小白去買醬油的時候,小白先等待,繼續打會遊戲,這時候如果小白媽讓小白買瓶醋回來,小白可以一次性帶着兩個需求出門,再把東西帶回來。

上面說的其實就是TCP的 Nagle 算法優化,目的是爲了避免發送小的數據包。

在 Nagle 算法開啓的狀態下,數據包在以下兩個情況會被髮送:

• 如果包長度達到MSS(或含有Fin包),立刻發送,否則等待下一個包到來;如果下一包到來後兩個包的總長度超過MSS的話,就會進行拆分發送;• 等待超時(一般爲200ms),第一個包沒到MSS長度,但是又遲遲等不到第二個包的到來,則立即發送。

Nagle2

• 由於啓動了 Nagle 算法,msg1 小於 mss ,此時等待200ms內來了一個 msg2,msg1 + msg2 > MSS,因此把 msg2 分爲 msg2(1) 和 msg2(2),msg1 + msg2(1) 包的大小爲MSS。此時發送出去。• 剩餘的 msg2(2) 也等到了 msg3,同樣 msg2(2) + msg3 > MSS,因此把 msg3 分爲 msg3(1) 和 msg3(2),msg2(2) + msg3(1) 作爲一個包發送。• 剩餘的 msg3(2) 長度不足mss,同時在200ms內沒有等到下一個包,等待超時,直接發送。• 此時三個包雖然在圖裏顏色不同,但是實際場景中,他們都是一整個 01 串,如果處理開發者把第一個收到的 msg1 + msg2(1) 就當做是一個完整消息進行處理,就會看上去就像是兩個包粘在一起,就會導致粘包問題

關掉 Nagle 算法就不會粘包了嗎?

Nagle 算法其實是個有些年代的東西了,誕生於 1984 年。對於應用程序一次發送一字節數據的場景,如果沒有 Nagle 的優化,這樣的包立馬就發出去了,會導致網絡由於太多的包而過載。

但是今天網絡環境比以前好太多,Nagle 的優化幫助就沒那麼大了。而且它的延遲發送,有時候還可能導致調用延時變大,比如打遊戲的時候,你操作如此絲滑,但卻因爲 Nagle 算法延遲發送導致慢了一拍,就問你難受不難受。

所以現在一般也會把它關掉

看起來,Nagle 算法的優化作用貌似不大,還會導致 ** 粘包 "問題"。那麼是不是關掉這個算法就可以解決掉這個粘包 "問題"** 呢?

TCP_NODELAY = 1

關閉 Nagle 就不會粘包了嗎

• 接受端應用層在收到 msg1 時立馬就取走了,那此時 msg1 沒粘包問題 •msg2 ** 到了後,應用層在忙,沒來得及取走,就呆在 TCP Recv Buffer 中了 •msg3 ** 此時也到了,跟 msg2 和 msg3 一起放在了 TCP Recv Buffer 中 • 這時候應用層忙完了,來取數據,圖裏是兩個顏色作區分,但實際場景中都是 01 串,此時一起取走,發現還是粘包。

因此,就算關閉 Nagle 算法,接收數據端的應用層沒有及時讀取 TCP Recv Buffer 中的數據,還是會發生粘包。

怎麼處理粘包

粘包出現的根本原因是不確定消息的邊界。接收端在面對 "無邊無際" 的二進制流的時候,根本不知道收了多少 01 纔算一個消息。一不小心拿多了就說是**粘包 **。其實粘包根本不是 TCP 的問題,是使用者對於 TCP 的理解有誤導致的一個問題。

只要在發送端每次發送消息的時候給消息帶上識別消息邊界的信息,接收端就可以根據這些信息識別出消息的邊界,從而區分出每個消息。

常見的方法有

• 加入特殊標誌

圖片

消息邊界頭尾標誌 可以通過特殊的標誌作爲頭尾,比如當收到了0xfffffe或者回車符,則認爲收到了新消息的頭,此時繼續取數據,直到收到下一個頭標誌0xfffffe或者尾部標記,才認爲是一個完整消息。類似的像 HTTP 協議裏當使用 chunked 編碼 傳輸時,使用若干個 chunk 組成消息,最後由一個標明長度爲 0 的 chunk 結束。• 加入消息長度信息

消息邊界長度標誌

這個一般配合上面的特殊標誌一起使用,在收到頭標誌時,裏面還可以帶上消息長度,以此表明在這之後多少 byte 都是屬於這個消息的。如果在這之後正好有符合長度的 byte,則取走,作爲一個完整消息給應用層使用。在實際場景中,HTTP 中的Content-Length就起了類似的作用,當接收端收到的消息長度小於 Content-Length 時,說明還有些消息沒收到。那接收端會一直等,直到拿夠了消息或超時,關於這一點上一篇文章裏有更詳細的說明。

可能這時候會有朋友會問,採用0xfffffe標誌位,用來標誌一個數據包的開頭,你就不怕你發的某個數據里正好有這個內容嗎?

是的,,所以一般除了這個標誌位,發送端在發送時還會加入各種校驗字段(校驗和或者對整段完整數據進行 CRC 之後獲得的數據)放在標誌位後面,在接收端拿到整段數據後校驗下確保它就是發送端發來的完整數據。

消息邊界頭尾加校驗標誌

UDP 會粘包嗎

跟 TCP 同爲傳輸層的另一個協議,UDP,User Datagram Protocol。用戶數據包協議,是面向無連接,不可靠的,基於數據報的傳輸層通信協議。

UDP 是什麼

基於數據報是指無論應用層交給 UDP 多長的報文,UDP 都照樣發送,即一次發送一個報文。至於如果數據包太長,需要分片,那也是 IP 層的事情,大不了效率低一些。UDP 對應用層交下來的報文,既不合並,也不拆分,而是保留這些報文的邊界。而接收方在接收數據報的時候,也不會像面對 TCP 無窮無盡的二進制流那樣不清楚啥時候能結束。正因爲基於數據報基於字節流的差異,TCP 發送端發 10 次字節流數據,而這時候接收端可以分 100 次去取數據,每次取數據的長度可以根據處理能力作調整;但 UDP 發送端發了 10 次數據報,那接收端就要在 10 次收完,且發了多少,就取多少,確保每次都是一個完整的數據報。

我們先看下 IP 報頭

ip 報頭

注意這裏面是有一個 16 位的總長度的,意味着 IP 報頭裏記錄了整個 IP 包的總長度。接着我們再看下 UDP 的報頭

UDP 報頭

在報頭中有16bit用於指示 UDP 數據報文的長度,假設這個長度是 n ,以此作爲數據邊界。因此在接收端的應用層能清晰地將不同的數據報文區分開,從報頭開始取 n 位,就是一個完整的數據報,從而避免粘包和拆包的問題。

當然,就算沒有這個位(16 位 UDP 長度),因爲 IP 的頭部已經包含了數據的總長度信息,此時如果 IP 包(網絡層)裏放的數據使用的協議是 UDP(傳輸層),那麼這個總長度其實就包含了 UDP 的頭部和 UDP 的數據。

因爲 UDP 的頭部長度固定爲 8 字節( 1 字節 = 8 位,8 字節 = 64 位,上圖中除了數據和選項以外的部分),那麼這樣就很容易的算出 UDP 的數據的長度了。因此說 UDP 的長度信息其實是冗餘的。

UDP 數據長度

UDP Data 的長度 = IP 總長度 - IP Header 長度 - UDP Header 長度

可以再來看下 TCP 的報頭

tcp 報頭 2

TCP 首部裏是沒有長度這個信息的,跟 UDP 類似,同樣可以通過下面的公式獲得當前包的 TCP 數據長度。

TCP Data 的長度 = IP 總長度 - IP Header 長度 - TCP Header 長度。

TCP 數據長度

跟 UDP 不同在於,TCP 發送端在發的時候就不保證發的是一個完整的數據報,僅僅看成一連串無結構的字節流,這串字節流在接收端收到時哪怕知道長度也沒用,因爲它很可能只是某個完整消息的一部分。

爲什麼長度字段冗餘還要加到 UDP 首部中

關於這一點,查了很多資料,《 TCP-IP 詳解(卷2)》裏說可能是因爲要用於計算校驗和。也有的說是因爲 UDP 底層使用的可以不是 IP 協議,畢竟 IP 頭裏帶了總長度,正好可以用於計算 UDP 數據的長度,萬一 UDP 的底層不是 IP 層協議,而是其他網絡層協議,就不能繼續這麼計算了。

但我覺得,最重要的原因是,IP 層是網絡層的,而 UDP 是傳輸層的,到了傳輸層,數據包就已經不存在 IP 頭信息了,那麼此時的 UDP 數據會被放在 UDP 的  Socket Buffer 中。當應用層來不及取這個 UDP 數據報,那麼兩個數據報在數據層面其實都是一堆 01 串。此時讀取第一個數據報的時候,會先讀取到 UDP 頭部,如果這時候 UDP 頭不含 UDP 長度信息,那麼應用層應該取多少數據纔算完整的一個數據報呢

因此 UDP 頭的這個長度其實跟 TCP 爲了防止粘包而在消息體里加入的邊界信息是起一樣的作用的。

爲什麼 UDP 要冗餘一個長度字段

面試的時候咱就把這些全說出去,顯得咱好像經過了深深的思考一樣,面試官可能會覺得咱特別愛思考,加分加分

如果我說錯了,請把我的這篇文章轉發給更多的人,讓大家記住這個滿嘴胡話的人,在關注之後狠狠的私信罵我,拜託了!

IP 層有粘包問題嗎

IP 層會對大包進行切片,是不是也有粘包問題?

先說結論,不會。首先前文提到了,粘包其實是由於使用者無法正確區分消息邊界導致的一個問題。

先看看 IP 層的切片分包是怎麼回事。

P 分包與重組

• 如果消息過長,IP層會按 MTU 長度把消息分成 N 個切片,每個切片帶有自身在包裏的位置(offset)同樣的 IP 頭信息。• 各個切片在網絡中進行傳輸。每個數據包切片可以在不同的路由中流轉,然後在最後的終點匯合後再組裝。• 在接收端收到第一個切片包時會申請一塊新內存,創建 IP 包的數據結構,等待其他切片分包數據到位。• 等消息全部到位後就把整個消息包給到上層(傳輸層)進行處理。

可以看出整個過程,IP 層從按長度切片到把切片組裝成一個數據包的過程中,都只管運輸,都不需要在意消息的邊界和內容,都不在意消息內容了,那就不會有粘包一說了。

IP 層表示:我只管把發送端給我的數據傳到接收端就完了,我也不瞭解裏頭放了啥東西。

聽起來就像 “我不管產品的需求傻不傻 X,我實現了就行,我不問,也懶得爭了”,這思路值得每一位優秀的划水程序員學習,respect

總結

粘包這個問題的根因是由於開發人員沒有正確理解 TCP 面向字節流的數據傳輸方式,本身並不是 TCP 的問題,是開發者的問題。

•TCP 不管發送端要發什麼,都基於字節流把數據發到接收端。這個字節流裏可能包含上一次想要發的數據的部分信息。接收端根據需要在消息里加上識別消息邊界的信息。不加就可能出現粘包問題。•TCP 粘包跟 Nagle 算法有關係,但關閉 Nagle 算法並不解決粘包問題。•UDP 是基於數據報的傳輸協議,不會有粘包問題。•IP 層也切片,但是因爲不關心消息裏有啥,因此有不會有粘包問題。•TCP 發送端可以發 10 次字節流數據,接收端可以分 100 次去取;UDP 發送端發了 10 次數據報,那接收端就要在 10 次收完。

數據包也只是按着 TCP 的方式進行組裝和拆分,如果數據包有錯,那數據包也只是犯了每個數據包都會犯的錯而已

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s?__biz=MzAwMDY4ODg5MA==&amp;mid=2247485943&amp;idx=1&amp;sn=f0d4b32c698bd3043018b7af5e2a06f3&amp;scene=21#wechat_redirect