1. 程式人生 > >TCP 的粘包與拆包問題

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