TCP粘包和拆包的處理方案

問題定義

TCP是一個「流」協議,所謂流,就是沒有界限的一長串二進制數據。TCP做爲傳輸層協議並不不瞭解上層業務數據的具體含義,它會根據TCP緩衝區的實際狀況進行數據包的劃分,因此在業務上認爲是一個完整的包,可能會被TCP拆分紅多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。git

理解什麼是粘包、拆包問題,先舉兩個簡單的應用場景:

假設應用層協議是httpgithub

我從瀏覽器中訪問了一個網站,網站服務器給我發了200k的數據。創建鏈接的時候,通告的MSS是50k,因此爲了防止ip層分片,tcp每次只會發送50k的數據,一共發了4個tcp數據包。若是我又訪問了另外一個網站,這個網站給我發了100k的數據,此次tcp會發出2個包,問題是,客戶端收到6個包,怎麼知道前4個包是一個頁面,後兩個是一個頁面。既然是tcp將這些包分開了,那tcp會將這些包重組嗎,它送給應用層的是什麼?算法

這是我本身想的一個場景,正式一點講的話,這個現象叫拆包。json

咱們再考慮一個問題。瀏覽器

tcp中有一個negal算法,用途是這樣的:通訊兩端有不少小的數據包要發送,雖然傳送的數據不多,可是流程一點沒少,也須要tcp的各類確認,校驗。這樣小的數據包若是不少,會形成網絡資源很大的浪費,negal算法作了這樣一件事,當來了一個很小的數據包,我不急於發送這個包,而是等來了更多的包,將這些小包組合成大包以後一併發送,不就提升了網絡傳輸的效率的嘛。這個想法收到了很好的效果,可是咱們想一下,若是是分屬於兩個不一樣頁面的包,被合併在了一塊兒,那客戶那邊如何區分它們呢?服務器

這就是粘包問題。網絡

從粘包問題咱們更能夠看出爲何tcp被稱爲流協議,由於它就跟水流同樣,是沒有邊界的,沒有消息的邊界保護機制,因此tcp只有流的概念,沒有包的概念。併發

咱們還須要有兩個概念tcp

長鏈接: Client方與Server方先創建通信鏈接,鏈接創建後不斷開, 而後再進行報文發送和接收。
短鏈接:Client方與Server每進行一次報文收發交易時才進行通信鏈接,交易完畢後當即斷開鏈接。此種方式經常使用於一點對多點 通信,好比多個Client鏈接一個Server.
下面咱們揭曉答案:性能

我想象的關於粘包的場景是不對的,http鏈接是短鏈接,請求以後,收到回答,立馬斷開鏈接,不會出現粘包。
拆包現象是有可能存在的

處理拆包

既然拆包現象可能存在,若是遇到了,那麼該如何處理呢?這裏提供兩種方法

經過包頭+包長+包體的協議形式,當服務器端獲取到指定的包長時才說明獲取完整。
指定包的結束標識,這樣當咱們獲取到指定的標識時,說明包獲取完整。

處理粘包

咱們從上面的分析看到,雖然像http這樣的短鏈接協議不會出現粘包的現象,可是一旦創建了長鏈接,粘包仍是有可能會發生的。

網上的處理方法有不少,這裏不列舉了,但你們看這些處理方法,都會發現,這些方法並很差,都會作一些犧牲。好比禁用negal算法,就是以網絡性能做爲犧牲。


  1. 客戶端和服務器創建一個鏈接,客戶端發送一條消息,客戶端關閉與服務器的鏈接。
  2. 客戶端和服務器創建一個鏈接,客戶端連續發送兩條消息,客戶端關閉與服務器的鏈接。

對於第一種狀況,服務器的處理流程能夠是這樣的:當客戶端與服務器的鏈接創建成功之後,服務器不斷讀取客戶端發送過來的數據,當客戶端與服務器鏈接斷開之後,服務器知道已經讀完了一條消息,而後進行解碼和後續處理。對於第二種狀況,若是按照上面相同的處理邏輯來處理,那就有問題了,咱們來看看第二種狀況下客戶端發送的兩條消息遞交到服務端有可能出現的狀況:

第一種狀況:

服務器一共讀到兩個數據包,第一個包包含客戶端發出的第一條消息的完整信息,第二個包包含客戶端發出的第二條消息,那這種狀況比較好處理,服務器只須要簡單的從網絡緩衝區去讀就行了,第一次讀到第一條消息的完整信息,消費完再從網絡緩衝區將第二條完整消息讀出來消費。

第二種狀況:

服務器一共就讀到一個數據包,這個數據包包含客戶端發出的兩條消息的完整性,這個時候基於以前邏輯實現的服務器就懵了。由於服務器不知道第一條消息從哪結束以及第二條消息從哪開始,這是發生了TCP粘包

第三種狀況:

服務器一共收到了兩個數據包,
第一個數據包只包含了第一條消息的一部分,第一條消息的後半部分和第二條消息都在第二個數據包中;
或者第一個數據包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數據包包含了第二條消息的剩下部分,這種狀況實際上是發送了TCP拆包。
由於發生了一條消息被拆分在了兩個包裏面發送了,一樣上面的服務器邏輯對於這種狀況是很差處理的。

產生TCP粘包和拆包的緣由

咱們知道TCP是以流動的方式傳輸數據的,傳輸的最小單位爲一個報文段(Segment)。TCP Header中有個Options標識位。常見的標識位爲MSS(Maximum Segment Size)指的是,鏈接層每次傳輸的數據有個最大限制MTU(Maximum Transmission Unit),通常是1500bit,超過這個量要分紅多個報文段,MSS則是這個最大限制減去TCP的header,光是要傳輸的數據的大小,通常爲1460bit。換算成字節,也就是180多字節。
TCP爲提升性能,發送端會將須要發送的數據發送到緩衝區,等待緩衝區滿了之後,再將緩衝中的數據發送到接收方。同理,接收方也有緩衝區這樣的機制來接受數據。
發生TCP粘包、拆包主要是如下緣由:
一、應用程序寫入數據大於套接字緩衝區大小,會發生拆包。
二、應用程序寫入數據小於套接字緩衝區大小,網卡將應用屢次寫入的數據發送到網絡上,這將會發送粘包。
三、進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP header長度>MSS 的時候會發生拆包。
四、接收方法不及時讀取套接字緩衝區數據,這將發生粘包。
......

如何解決拆包、粘包

既然知道TCP是無界的數據流,且協議自己沒法避免粘(拆)包的發生。那咱們只能再應用層數據協議上加以控制。一般再製定傳輸數據時,可使用以下方法:
一、使用帶消息頭的協議。消息頭存儲消息開始標識消息長度信息,服務器獲取消息頭的時候解析出消息長度,而後向後讀取該長度的內容。
二、設置定長消息。服務器每次讀取既定長度的內容做爲一條完整消息。
三、設置消息邊界。服務器從網絡流中按消息編輯分離出消息內容。

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的數據格式的包體數據。

參考:
tcp是流的一些思考--拆包和粘包
tcp粘包和拆包的處理方案

相關文章
相關標籤/搜索