TCP 的粘包與拆包問題

之前在做項目時,使用 Java NIO 來搭建服務器端及客戶端程序,發現待發送的數據大於發送緩衝區 ByteBuffer 大小時,將發生拆包情況,會把待發送的數據包分多次發送到客戶端。當時是分配了更大的字節緩衝區來解決這個問題,後來瞭解到這是 TCP 協議中的粘包與拆包問題。首先我們瞭解一下 TCP 的特性。

TCP 特性

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

面向連接:通信雙方在應用 TCP 協議進行通信之前需要通過三次握手來建立 TCP 連接,連接建立後才能進行正常的數據傳輸。

可靠性:由於 TCP 處於 IP 層之上,而 IP 層提供不可靠的傳輸,因此在 TCP 層存在四種常見傳輸錯誤,分別是比特錯誤(packet bit errors)、包亂序(packet reordering)、包重複(packet duplication)、丟包(packet drops),因此 TCP 要提供可靠的傳輸,就需要具有超時與重傳管理、窗口管理、流量控制、擁塞控制等功能。

可靠性體現在三方面,首先 TCP 通過超時重傳和快速重傳兩個常見手段來保證數據包的正確傳輸,即接收端在沒有收到數據包或者收到錯誤的數據包時會觸發發送端的數據包重傳(處理比特錯誤和丟包);其次 TCP 接收端會緩存接收到的亂序到達數據,重排序後再向應用層提供有序的數據(處理包亂序);最後 TCP 發送端會維持一個發送窗口動態的調整發送速率以適用接收端緩存限制和網絡擁塞情況,避免了網絡擁塞或者接收端緩存滿而大量丟包的情況(降低丟包率)。

字節流式:應用層發送的數據會再 TCP 的發送端緩存起來,統一分片(如一個應用層的數據包分成兩個 TCP 包,即拆包)或者打包(如兩個或者多個應用層的數據包打包成一個 TCP 數據包,即粘包)發送,到接收端的時候接收端也是直接按照字節流將數據傳遞給應用層。UDP 並不會對應用層的數據包進行打包和分片操作,一般一個應用層的數據包就對應一個 UDP 包。

粘包與拆包

UDP 是基於報文發送的,從 UDP 的幀結構可以看出,在 UDP 報文的首部採用了 16 bit 來指示 UDP 數據報文的長度,不同的報文之間是可以區分隔離出來的,所以應用層在接收傳輸層的報文時,不會存在拆包與粘包的問題。

而 TCP 是基於字節流傳輸的,應用層和傳輸層之間數據交互是大小不等的數據塊,但 TCP 把這些數據塊僅僅看成一連串無結構的字節流,沒有邊界,所以它不知道哪些數據塊跟哪些數據塊應該一起發送,哪些數據塊應該是單獨發送的;從 TCP 的幀結構也可以看出,TCP 首部沒有表示數據長度的字段,即 TCP 並沒有像 UDP 那樣首部有數據長度,所以 TCP 存在拆包和粘包的問題。

關於 UDP 幀結構與 TCP 幀結構的詳情大家可以參考https://mp.csdn.net/postedit/83656881這篇博客。 

粘包與拆包表現形式

現在假設客戶端向服務端連續發送了兩個數據包,用 packet1 和 packet2 來表示,那麼服務端收到的數據可以分爲以下三種,如下所示:

第一種:接收端正常收到兩個數據包,沒有發生拆包和粘包情況,此種情況本處不討論。

第二種:接收端只收到一個數據包,TCP 把兩個數據包合併成一個發送給接收端了,這一個數據包中包含了發送端發送的兩個數據包的信息,這種情況稱爲粘包,由於接收端不知道這兩個數據包之間的分隔界限,所以對於接收端來說是很難處理的。 

第三種:這種情況有兩種表現形式,接收端收到了兩個數據包,但是這兩個數據包要麼是不完整的,要麼就是多出一部分,這種情況發生了拆包與粘包。這種情況如果不加特殊處理,接收端同樣是不好處理的。 

粘包與拆包發生的原因 

1. 要發送的數據包大於 TCP 發送緩存區的可用空間大小時,數據會發生拆包;

2. 要發送的數據大於 MSS(Maximum Segment Size)最大報文段時,TCP 在發送前會對數據進行拆包;

TCP 在三次握手建立連接過程中,會在 SYN(同步序號) 報文中使用 MSS 選項功能,協商建立連接雙方能夠接收的最大報文段 MSS 的值,MSS 是傳輸層 TCP 協議範疇內的概念,它是標識 TCP 能夠承載的最大的應用數據段長度,有 MSS = MTU (最大傳送單元,一般是 1500 bit ,超過這個量要分成多個報文段) - 20 字節 TCP 報頭 - 20 字節 IP 報頭,那麼在以太網環境下,MSS 值一般就是 1500 - 20 - 20 = 1460 字節。

3. 發送的數據小於 TCP 緩存區大小時,TCP 會將幾次寫入緩衝區的數據一次性發送,會存在粘包;

4. 接收數據端的應用層沒有及時讀取接收緩存區中的數據時,會發生粘包。

粘包與拆包解決方法

由於傳輸層的 TCP 無法理解應用層的業務數據,所以在傳輸層是無法保證數據包不被拆分和重組的,那麼該問題只能通過應用層協議棧設計解決,給數據包加分界標記,來處理最後接收到的數據,不管拆分還是粘包都可以很好處理。

1. 發送端給數據包增加首部,首部包含數據包中數據的長度,這樣接收端的應用層在接收到數據後,根據首部中的長度就可以知道數據的實際長度了,可以很好處理數據。設計思路,可以在首部固定 10 個字節長度用來保存整個數據包長度,位數不夠補0。

0000000042{"type":"message","content":"hello"}

2. 設置數據包的長度爲固定長度,不夠數據則以0填充,這樣接收端每次從接收緩衝區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。

3. 應用層在發送每個數據包時,給每個數據包加分界標記,如換行符 "\n",這樣接收端通過這個分界標記就可以將不同的數據包拆分開來了。如下是一個符合這個規則的請求包(需注意請求數據內部本身不能包含換行符,數據格式爲 Json)。

{"type":"message","content":"HelloWorld!"}\n

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章