Netty之路(二)TCP拆包/粘包問題

TCP傳輸協議是面向流的,就是沒有界限的一串數據。TCP底層并不了解上層業務數據的具體含義,它會根據TCP緩沖區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包就行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP拆包和粘包問題。

TCP拆包/粘包問題

image.png

假設客戶端分別發送了兩個數據包D1和D2給服務端,由于服務端一次讀取到字節數是不確定的,故可能存在以下四種情況:

服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包
服務端一次接受到了兩個數據包,D1和D2粘合在一起,稱之為TCP粘包
服務端分兩次讀取到了數據包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩余內容,這稱之為TCP拆包
服務端分兩次讀取到了數據包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩余部分內容D1_2和完整的D2包。
特別要注意的是,如果TCP的接受滑窗非常小,而數據包D1和D2比較大,很有可能會發生第五種情況,即服務端分多次才能將D1和D2包完全接受,期間發生多次拆包。

TCP拆包/粘包發生的原因

在網絡通信的過程中,每次可以發送的數據包大小是受多種因素限制的,如 MTU 傳輸單元大小、MSS 最大分段大小、滑動窗口等。如果一次傳輸的網絡包數據大小超過傳輸單元大小,那么我們的數據可能會拆分為多個數據包發送出去。如果每次請求的網絡包數據都很小,一共請求了 10000 次,TCP 并不會分別發送 10000 次。因為 TCP 采用的 Nagle 算法對此作出了優化。

MTU 最大傳輸單元和 MSS 最大分段大小

MTU(Maxitum Transmission Unit) 是鏈路層一次最大傳輸數據的大小。MTU 一般來說大小為 1500 byte。MSS(Maximum Segement Size) 是指 TCP 最大報文段長度,它是傳輸層一次發送最大數據的大小。如下圖所示,MTU 和 MSS 一般的計算關系為:MSS = MTU - IP 首部 - TCP首部,如果 MSS + TCP 首部 + IP 首部 > MTU,那么數據包將會被拆分為多個發送。這就是拆包現象。


image.png

滑動窗口

滑動窗口是 TCP 傳輸層用于流量控制的一種有效措施,也被稱為通告窗口?;瑒哟翱谑菙祿邮辗皆O置的窗口大小,隨后接收方會把窗口大小告訴發送方,以此限制發送方每次發送數據的大小,從而達到流量控制的目的。這樣數據發送方不需要每發送一組數據就阻塞等待接收方確認,允許發送方同時發送多個數據分組,每次發送的數據都會被限制在窗口大小內。由此可見,滑動窗口可以大幅度提升網絡吞吐量。

現在來看一下滑動窗口是如何造成粘包、拆包的?

粘包:假設發送方的每256 bytes表示一個完整的報文,接收方由于數據處理不及時,這256個字節的數據都會被緩存到SO_RCVBUF(接收緩存區)中。如果接收方的SO_RCVBUF中緩存了多個報文,那么對于接收方而言,這就是粘包。

拆包:考慮另外一種情況,假設接收方的窗口只剩了128,意味著發送方最多還可以發送128字節,而由于發送方的數據大小是256字節,因此只能發送前128字節,等到接收方ack后,才能發送剩余字節。這就造成了拆包。

Nagle算法

TCP/IP協議中,無論發送多少數據,總是要在數據(DATA)前面加上協議頭(TCP Header+IP Header),同時,對方接收到數據,也需要發送ACK表示確認。

即使從鍵盤輸入的一個字符,占用一個字節,可能在傳輸上造成41字節的包,其中包括1字節的有用信息和40字節的首部數據。這種情況轉變成了4000%的消耗,這樣的情況對于重負載的網絡來是無法接受的。

為了盡可能的利用網絡帶寬,TCP總是希望盡可能的發送足夠大的數據。(一個連接會設置MSS參數,因此,TCP/IP希望每次都能夠以MSS尺寸的數據塊來發送數據)。

Nagle算法就是為了盡可能發送大塊數據,避免網絡中充斥著許多小數據塊。

Nagle算法的基本定義是任意時刻,最多只能有一個未被確認的小段。 所謂“小段”,指的是小于MSS尺寸的數據塊,所謂“未被確認”,是指一個數據塊發送出去后,沒有收到對方發送的ACK確認該數據已收到。

Nagle算法的規則:

  • 如果SO_SNDBUF(發送緩沖區)中的數據長度達到MSS,則允許發送;
  • 如果該SO_SNDBUF中含有FIN,表示請求關閉連接,則先將SO_SNDBUF中的剩余數據發送,再關閉;
  • 設置了TCP_NODELAY=true選項,則允許發送。TCP_NODELAY是取消TCP的確認延遲機制,相當于禁用了Nagle 算法。
  • 未設置TCP_CORK選項時,若所有發出去的小數據包(包長度小于MSS)均被確認,則允許發送;
  • 上述條件都未滿足,但發生了超時(一般為200ms),則立即發送。

TCP拆包/粘包問題的解決策略

由于底層的TCP無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,可以歸納如下:

  • 消息定長,例如每個報文的大小為固定長度200字節,如果不夠,空位補空格。假設我們需要發送的數據是| AB | CDEF | GHIJ | K | LM | 5條數據,我們的固定長度為 4 字節,那么這5 條數據一共需要發送 4 個報文:
+------+------+------+------+

| ABCD | EFGH | IJKL | M000 |

+------+------+------+------+

消息定長法使用非常簡單,但是缺點也非常明顯,無法很好設定固定長度的值,如果長度太大會造成字節浪費,長度太小又會影響消息傳輸,所以在一般情況下消息定長法不會被采用。

  • 在包尾增加回車換行符進行分割,以下例子采用\n來分割
+-------------------------+

| AB\nCDEF\nGHIJ\nK\nLM\n |

+-------------------------+

由于在發送報文時尾部需要添加特定分隔符,所以對于分隔符的選擇一定要避免和消息體中字符相同,以免沖突。否則可能出現錯誤的消息拆分。比較推薦的做法是將消息進行編碼,例如 base64 編碼,然后可以選擇 64 個編碼字符之外的字符作為特定分隔符。特定分隔符法在消息協議足夠簡單的場景下比較高效,例如大名鼎鼎的 Redis 在通信過程中采用的就是換行分隔符。

  • 將消息分為消息頭和消息體,消息頭中包含表示消息總長度(或消息體長度)的字段,通常設計思路為消息頭的第一個字段使用int32表示消息的總長度
消息頭     消息體

+--------+----------+

| Length |  Content |

+--------+----------+

+-----+-------+-------+----+-----+

| 2AB | 4CDEF | 4GHIJ | 1K | 2LM |

+-----+-------+-------+----+-----+

消息長度 + 消息內容的使用方式非常靈活,且不會存在消息定長法和特定分隔符法的明顯缺陷。當然在消息頭中不僅只限于存放消息的長度,而且可以自定義其他必要的擴展字段,例如消息版本、算法類型等。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 熟悉tcp編程的可能都知道,無論是服務器端還是客戶端,當我們讀取或者發送數據的時候,都需要考慮TCP底層的粘包/拆...
    彬榮閱讀 373評論 0 1
  • 一、TCP 粘包和拆包基本介紹 TCP是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有...
    小波同學閱讀 1,410評論 2 10
  • TCP底層的粘包/拆包機制 其實很多熟悉TCP編程的小伙伴們都知道,無論是客戶端還是服務端,當我們讀取或者發送數據...
    櫻桃還是饅頭閱讀 315評論 0 0
  • 什么是粘包、拆包? 對于什么是粘包、拆包問題,我想先舉兩個簡單的應用場景: 客戶端和服務器建立一個連接,客戶端發送...
    昨日已逝去閱讀 2,194評論 0 2
  • ????TCP是個“流”協議,所謂流,就是沒有界限的一串數據.TCP底層并不了解上層業務數據的具體含義,它會根據T...
    劉澤田閱讀 239評論 0 1