問題定義
TCP是一個“流”協議,所謂流,就是沒有界限的一長串二進制數據。TCP作為傳輸層協議並不不了解上層業務數據的具體含義,它會根據TCP緩沖區的實際情況進行數據包的划分,所以在業務上認為是一個完整的包,可能會被TCP拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。
理解什么是粘包、拆包問題,先舉兩個簡單的應用場景:
假設應用層協議是http
我從瀏覽器中訪問了一個網站,網站服務器給我發了200k的數據。建立連接的時候,通告的MSS是50k,所以為了防止ip層分片,tcp每次只會發送50k的數據,一共發了4個tcp數據包。如果我又訪問了另一個網站,這個網站給我發了100k的數據,這次tcp會發出2個包,問題是,客戶端收到6個包,怎么知道前4個包是一個頁面,后兩個是一個頁面。既然是tcp將這些包分開了,那tcp會將這些包重組嗎,它送給應用層的是什么?
這是我自己想的一個場景,正式一點講的話,這個現象叫拆包。
我們再考慮一個問題。
tcp中有一個negal算法,用途是這樣的:通信兩端有很多小的數據包要發送,雖然傳送的數據很少,但是流程一點沒少,也需要tcp的各種確認,校驗。這樣小的數據包如果很多,會造成網絡資源很大的浪費,negal算法做了這樣一件事,當來了一個很小的數據包,我不急於發送這個包,而是等來了更多的包,將這些小包組合成大包之后一並發送,不就提高了網絡傳輸的效率的嘛。這個想法收到了很好的效果,但是我們想一下,如果是分屬於兩個不同頁面的包,被合並在了一起,那客戶那邊如何區分它們呢?
這就是粘包問題。
從粘包問題我們更可以看出為什么tcp被稱為流協議,因為它就跟水流一樣,是沒有邊界的,沒有消息的邊界保護機制,所以tcp只有流的概念,沒有包的概念。
我們還需要有兩個概念
長連接: Client方與Server方先建立通訊連接,連接建立后不斷開, 然后再進行報文發送和接收。
短連接:Client方與Server每進行一次報文收發交易時才進行通訊連接,交易完畢后立即斷開連接。此種方式常用於一點對多點 通訊,比如多個Client連接一個Server.
下面我們揭曉答案:
我想象的關於粘包的場景是不對的,http連接是短連接,請求之后,收到回答,立馬斷開連接,不會出現粘包。
拆包現象是有可能存在的
處理拆包
既然拆包現象可能存在,如果遇到了,那么該如何處理呢?這里提供兩種方法
通過包頭+包長+包體的協議形式,當服務器端獲取到指定的包長時才說明獲取完整。
指定包的結束標識,這樣當我們獲取到指定的標識時,說明包獲取完整。
處理粘包
我們從上面的分析看到,雖然像http這樣的短連接協議不會出現粘包的現象,但是一旦建立了長連接,粘包還是有可能會發生的。
網上的處理方法有很多,這里不列舉了,但大家看這些處理方法,都會發現,這些方法並不好,都會做一些犧牲。比如禁用negal算法,就是以網絡性能作為犧牲。
- 客戶端和服務器建立一個連接,客戶端發送一條消息,客戶端關閉與服務器的連接。
- 客戶端和服務器建立一個連接,客戶端連續發送兩條消息,客戶端關閉與服務器的連接。
對於第一種情況,服務器的處理流程可以是這樣的:當客戶端與服務器的連接建立成功以后,服務器不斷讀取客戶端發送過來的數據,當客戶端與服務器連接斷開以后,服務器知道已經讀完了一條消息,然后進行解碼和后續處理。對於第二種情況,如果按照上面相同的處理邏輯來處理,那就有問題了,我們來看看第二種情況下客戶端發送的兩條消息遞交到服務端有可能出現的情況:
第一種情況:
服務器一共讀到兩個數據包,第一個包包含客戶端發出的第一條消息的完整信息,第二個包包含客戶端發出的第二條消息,那這種情況比較好處理,服務器只需要簡單的從網絡緩沖區去讀就好了,第一次讀到第一條消息的完整信息,消費完再從網絡緩沖區將第二條完整消息讀出來消費。
第二種情況:
服務器一共就讀到一個數據包,這個數據包包含客戶端發出的兩條消息的完整性,這個時候基於之前邏輯實現的服務器就懵了。因為服務器不知道第一條消息從哪結束以及第二條消息從哪開始,這是發生了TCP粘包。
第三種情況:
服務器一共收到了兩個數據包,
第一個數據包只包含了第一條消息的一部分,第一條消息的后半部分和第二條消息都在第二個數據包中;
或者第一個數據包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數據包包含了第二條消息的剩下部分,這種情況其實是發送了TCP拆包。
因為發生了一條消息被拆分在了兩個包里面發送了,同樣上面的服務器邏輯對於這種情況是不好處理的。
產生TCP粘包和拆包的原因
我們知道TCP是以流動的方式傳輸數據的,傳輸的最小單位為一個報文段(Segment)。TCP Header中有個Options標識位。常見的標識位為MSS(Maximum Segment Size)指的是,連接層每次傳輸的數據有個最大限制MTU(Maximum Transmission Unit),一般是1500bit,超過這個量要分成多個報文段,MSS則是這個最大限制減去TCP的header,光是要傳輸的數據的大小,一般為1460bit。換算成字節,也就是180多字節。
TCP為提高性能,發送端會將需要發送的數據發送到緩沖區,等待緩沖區滿了以后,再將緩沖中的數據發送到接收方。同理,接收方也有緩沖區這樣的機制來接受數據。
發生TCP粘包、拆包主要是以下原因:
1、應用程序寫入數據大於套接字緩沖區大小,會發生拆包。
2、應用程序寫入數據小於套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發送粘包。
3、進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP header長度>MSS 的時候會發生拆包。
4、接收方法不及時讀取套接字緩沖區數據,這將發生粘包。
......
如何解決拆包、粘包
既然知道TCP是無界的數據流,且協議本身無法避免粘(拆)包的發生。那我們只能再應用層數據協議上加以控制。通常再制定傳輸數據時,可以使用如下方法:
1、使用帶消息頭的協議。消息頭存儲消息開始標識及消息長度信息,服務器獲取消息頭的時候解析出消息長度,然后向后讀取該長度的內容。
2、設置定長消息。服務器每次讀取既定長度的內容作為一條完整消息。
3、設置消息邊界。服務器從網絡流中按消息編輯分離出消息內容。
a)先基於第3種方法,假設區分數據邊界的標識為換行符"\n"(【注意】:請求數據本身內部不能包含換行符),數據格式為JSON。如下是一個符合該規則的請求包。
{"type":"message","content":"hello"}\n
(\n代表一個請求的結束)
b)基於第1種方法,可以制定,首部固定10的字節長度用來保存整個數據包長度,位數不夠補0的數據協議。
0000000036{"type":"message","content":"hello"}
c)基於第1種方法。可以制定,首部4字節網絡字節序unsigned int,標記整個包的長度
****{"type":"message","content":"hello all"}
其中首部4字節*代表一個網闊字節序的unsigned int數據,為不可見字符,緊接着是JSON的數據格式的包體數據。