事情從一個健身教練提及吧。程序員
李東,自稱亞健康終結者,嘗試使用互聯網+的模式拓展本身的業務。在某款新開發的聊天軟件琛琛上發佈廣告。golang
鍵盤說來就來。瘋狂發送"李東",回車發送!,"亞健康終結者",再回車發送!面試
還記得四層網絡協議長什麼樣子嗎?算法
四層網絡模型每層各司其職,消息在進入每一層時都會多加一個報頭,每多一個報頭能夠理解爲數據報多戴一頂帽子。這個報頭上面記錄着消息從哪來,到哪去,以及消息多長等信息。好比,mac頭部
記錄的是硬件的惟一地址,IP頭
記錄的是從哪來和到哪去,傳輸層頭記錄到是到達目的主機後具體去哪一個進程。網絡
在從消息發到網絡的時候給消息帶上報頭,消息和紛繁複雜的網絡中經過這些信息在路由器間流轉,最後到達目的機器上,接受者再經過這些報頭,一步一步還原出發送者最原始要發送的消息。數據結構
軟件琛琛是屬於應用層上的。tcp
而"李東","亞健康終結者"這兩條消息在進入傳輸層時使用的是傳輸層上的 TCP 協議。消息在進入傳輸層(TCP)時會被切片爲一個個數據包。這個數據包的長度是MSS
。學習
能夠把網絡比喻爲一個水管,是有必定的粗細的,這個粗細由網絡接口層(數據鏈路層)提供給網絡層,通常認爲是的MTU
(1500),直接傳入整個消息,會超過水管的最大承受範圍,那麼,就須要進行切片,成爲一個個數據包,這樣消息才能正常經過「水管」。優化
那麼當李東在手機上鍵入"李東""亞健康終結者"的時候,在 TCP 中把消息分紅 MSS 大小後,消息順着網線順利發出。編碼
網絡穩得很,將消息分片傳到了對端手機 B 上。通過 TCP 層消息重組。變成"李東亞健康終結者"這樣的字節流(stream)。
但因爲聊天軟件琛琛是新開發的,並且開發者叫小白,完了,是個臭名昭著的造 bug 工程師。通過他的代碼,在處理字節流的時候消息從"李東","亞健康終結者"變成了"李東亞","健康終結者"。"李東"做爲上一個包的內容與下一個包裏的"亞"粘在了一塊兒被錯誤地當成了一個數據包解析了出來。這就是所謂的粘包。
一個號稱健康終結者的健身教練,大概運氣也不會不好吧,就祝他客源滾滾吧。
那就要從 TCP 是啥提及。
TCP,Transmission Control Protocol。傳輸控制協議,是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議。
其中跟粘包關係最大的就是基於字節流這個特色。
字節流能夠理解爲一個雙向的通道里流淌的數據,這個數據其實就是咱們常說的二進制數據,簡單來講就是一大堆 01 串。這些 01 串之間沒有任何邊界。
應用層傳到 TCP 協議的數據,不是以消息報爲單位向目的主機發送,而是以字節流的方式發送到下游,這些數據可能被切割和組裝成各類數據包,接收端收到這些數據包後沒有正確還原原來的消息,所以出現粘包現象。
上面提到 TCP 切割數據包是爲了能順利經過網絡這根水管。相反,還有一個組裝的狀況。若是先後兩次 TCP 發的數據都遠小於 MSS,好比就幾個字節,每次都單獨發送這幾個字節,就比較浪費網絡 io 。
好比小白爸讓小白出門給買一瓶醬油,小白出去買醬油回來了。小白媽又讓小白出門買一瓶醋回來。小白先後結結實實跑了兩趟,影響了打遊戲的時間。
優化的方法也比較簡單。當小白爸讓小白去買醬油的時候,小白先等待,繼續打會遊戲,這時候若是小白媽讓小白買瓶醋回來,小白能夠一次性帶着兩個需求出門,再把東西帶回來。
上面說的其實就是TCP
的 Nagle 算法優化,目的是爲了不發送小的數據包。
在 Nagle 算法開啓的狀態下,數據包在如下兩個狀況會被髮送:
MSS
(或含有Fin
包),馬上發送,不然等待下一個包到來;若是下一包到來後兩個包的總長度超過MSS
的話,就會進行拆分發送;200ms
),第一個包沒到MSS
長度,可是又遲遲等不到第二個包的到來,則當即發送。
200ms
內來了一個 msg2 ,msg1 + msg2 > MSS,所以把 msg2 分爲 msg2(1) 和 msg2(2),msg1 + msg2(1) 包的大小爲MSS
。此時發送出去。mss
,同時在200ms
內沒有等到下一個包,等待超時,直接發送。Nagle 算法實際上是個有些年代的東西了,誕生於 1984 年。對於應用程序一次發送一字節數據的場景,若是沒有 Nagle 的優化,這樣的包立馬就發出去了,會致使網絡因爲太多的包而過載。
可是今天網絡環境比之前好太多,Nagle 的優化幫助就沒那麼大了。並且它的延遲發送,有時候還可能致使調用延時變大,好比打遊戲的時候,你操做如此絲滑,但卻由於 Nagle 算法延遲發送致使慢了一拍,就問你難受不難受。
因此如今通常也會把它關掉。
看起來,Nagle 算法的優化做用貌似不大,還會致使粘包"問題"。那麼是否是關掉這個算法就能夠解決掉這個粘包"問題"呢?
TCP_NODELAY = 1
所以,就算關閉 Nagle 算法,接收數據端的應用層沒有及時讀取 TCP Recv Buffer 中的數據,仍是會發生粘包。
粘包出現的根本緣由是不肯定消息的邊界。接收端在面對"一望無際"的二進制流的時候,根本不知道收了多少 01 纔算一個消息。一不當心拿多了就說是粘包。其實粘包根本不是 TCP 的問題,是使用者對於 TCP 的理解有誤致使的一個問題。
只要在發送端每次發送消息的時候給消息帶上識別消息邊界的信息,接收端就能夠根據這些信息識別出消息的邊界,從而區分出每一個消息。
常見的方法有
能夠經過特殊的標誌做爲頭尾,好比當收到了0xfffffe
或者回車符,則認爲收到了新消息的頭,此時繼續取數據,直到收到下一個頭標誌0xfffffe
或者尾部標記,才認爲是一個完整消息。相似的像 HTTP 協議裏當使用 chunked 編碼 傳輸時,使用若干個 chunk 組成消息,最後由一個標明長度爲 0 的 chunk 結束。
這個通常配合上面的特殊標誌一塊兒使用,在收到頭標誌時,裏面還能夠帶上消息長度,以此代表在這以後多少 byte 都是屬於這個消息的。若是在這以後正好有符合長度的 byte,則取走,做爲一個完整消息給應用層使用。在實際場景中,HTTP 中的Content-Length
就起了相似的做用,當接收端收到的消息長度小於 Content-Length 時,說明還有些消息沒收到。那接收端會一直等,直到拿夠了消息或超時,關於這一點上一篇文章裏有更詳細的說明。
可能這時候會有朋友會問,採用0xfffffe
標誌位,用來標誌一個數據包的開頭,你就不怕你發的某個數據里正好有這個內容嗎?
是的,怕,因此通常除了這個標誌位,發送端在發送時還會加入各類校驗字段(校驗和
或者對整段完整數據進行 CRC
以後得到的數據)放在標誌位後面,在接收端拿到整段數據後校驗下確保它就是發送端發來的完整數據。
跟 TCP
同爲傳輸層的另外一個協議,UDP,User Datagram Protocol。用戶數據包協議,是面向無鏈接,不可靠的,基於數據報的傳輸層通訊協議。
基於數據報是指不管應用層交給 UDP 多長的報文,UDP 都照樣發送,即一次發送一個報文。至於若是數據包太長,須要分片,那也是IP層的事情,大不了效率低一些。UDP 對應用層交下來的報文,既不合並,也不拆分,而是保留這些報文的邊界。而接收方在接收數據報的時候,也不會像面對 TCP 無窮無盡的二進制流那樣不清楚啥時候能結束。正由於基於數據報和基於字節流的差別,TCP 發送端發 10 次字節流數據,而這時候接收端能夠分 100 次去取數據,每次取數據的長度能夠根據處理能力做調整;但 UDP 發送端發了 10 次數據報,那接收端就要在 10 次收完,且發了多少,就取多少,確保每次都是一個完整的數據報。
咱們先看下IP報頭
注意這裏面是有一個 16 位的總長度的,意味着 IP 報頭裏記錄了整個 IP 包的總長度。接着咱們再看下 UDP 的報頭。
在報頭中有16bit
用於指示 UDP 數據報文的長度,假設這個長度是 n ,以此做爲數據邊界。所以在接收端的應用層能清晰地將不一樣的數據報文區分開,從報頭開始取 n 位,就是一個完整的數據報,從而避免粘包和拆包的問題。
固然,就算沒有這個位(16位 UDP 長度),由於 IP 的頭部已經包含了數據的總長度信息,此時若是 IP 包(網絡層)裏放的數據使用的協議是 UDP(傳輸層),那麼這個總長度其實就包含了 UDP 的頭部和 UDP 的數據。
由於 UDP 的頭部長度固定爲 8 字節( 1 字節= 8 位,8 字節= 64 位,上圖中除了數據和選項
之外的部分),那麼這樣就很容易的算出 UDP 的數據的長度了。所以說 UDP 的長度信息實際上是冗餘的。
UDP Data 的長度 = IP 總長度 - IP Header 長度 - UDP Header 長度
能夠再來看下 TCP 的報頭
TCP首部裏是沒有長度這個信息的,跟UDP相似,一樣能夠經過下面的公式得到當前包的TCP數據長度。
TCP Data 的長度 = IP 總長度 - IP Header 長度 - TCP Header 長度。
跟 UDP 不一樣在於,TCP 發送端在發的時候就不保證發的是一個完整的數據報,僅僅當作一連串無結構的字節流,這串字節流在接收端收到時哪怕知道長度也沒用,由於它極可能只是某個完整消息的一部分。
關於這一點,查了不少資料,《 TCP-IP 詳解(卷2)》
裏說多是由於要用於計算校驗和。也有的說是由於UDP底層使用的能夠不是IP協議,畢竟 IP 頭裏帶了總長度,正好能夠用於計算 UDP 數據的長度,萬一 UDP 的底層不是IP層協議,而是其餘網絡層協議,就不能繼續這麼計算了。
但我以爲,最重要的緣由是,IP 層是網絡層的,而 UDP 是傳輸層的,到了傳輸層,數據包就已經不存在IP頭信息了,那麼此時的UDP數據會被放在 UDP 的 Socket Buffer
中。當應用層來不及取這個 UDP 數據報,那麼兩個數據報在數據層面其實都是一堆 01 串。此時讀取第一個數據報的時候,會先讀取到 UDP 頭部,若是這時候 UDP 頭不含 UDP 長度信息,那麼應用層應該取多少數據纔算完整的一個數據報呢?
所以 UDP 頭的這個長度其實跟 TCP 爲了防止粘包而在消息體里加入的邊界信息是起同樣的做用的。
面試的時候咱就把這些全說出去,顯得咱好像通過了深深的思考同樣,面試官可能會以爲咱特別愛思考,加分加分。
若是我說錯了,請把個人這篇文章轉發給更多的人,讓你們記住這個滿嘴胡話的人,在關注以後狠狠的私信罵我,拜託了!
IP 層會對大包進行切片,是否是也有粘包問題?
先說結論,不會。首先前文提到了,粘包實際上是因爲使用者沒法正確區分消息邊界致使的一個問題。
先看看 IP 層的切片分包是怎麼回事。
IP層
會按 MTU 長度把消息分紅 N 個切片,每一個切片帶有自身在包裏的位置(offset)和一樣的IP頭信息。能夠看出整個過程,IP 層
從按長度切片到把切片組裝成一個數據包的過程當中,都只管運輸,都不須要在乎消息的邊界和內容,都不在乎消息內容了,那就不會有粘包一說了。
IP 層
表示:我只管把發送端給個人數據傳到接收端就完了,我也不瞭解裏頭放了啥東西。
聽起來就像 「我無論產品的需求傻不傻X,我實現了就行,我不問,也懶得爭了」,這思路值得每一位優秀的划水程序員學習,respect。
粘包這個問題的根因是因爲開發人員沒有正確理解 TCP 面向字節流的數據傳輸方式,自己並非 TCP 的問題,是開發者的問題。
TCP
發送端能夠發 10 次
字節流數據,接收端能夠分 100 次
去取;UDP
發送端發了 10 次
數據報,那接收端就要在 10 次
收完。數據包也只是按着 TCP 的方式進行組裝和拆分,若是數據包有錯,那數據包也只是犯了每一個數據包都會犯的錯而已。
最後,李東工做沒了,而小白表示
關注公衆號:【golang小白成長記】