1. 程式人生 > 其它 >TCP 協議如何解決粘包、半包問題

TCP 協議如何解決粘包、半包問題

一、TCP 協議是流式協議

  很多讀者從接觸網路知識以來,應該聽說過這句話:TCP 協議是流式協議。那麼這句話到底是什麼意思呢?所謂流式協議,即協議的內容是像流水一樣的位元組流,內容與內容之間沒有明確的分界標誌,需要我們人為地去給這些協議劃分邊界

  舉個例子,A 與 B 進行 TCP 通訊,A 先後給 B 傳送了一個 100 位元組和 200 位元組的資料包,那麼 B 是如何收到呢?B 可能先收到 100 位元組,再收到 200 位元組;也可能先收到 50 位元組,再收到 250 位元組;或者先收到 100 位元組,再收到 100 位元組,再收到 200 位元組;或者先收到 20 位元組,再收到 20 位元組,再收到 60 位元組,再收到 100 位元組,再收到 50 位元組,再收到 50 位元組……

  不知道讀者看出規律沒有?規律就是 A 一共給 B 傳送了 300 位元組,B 可能以一次或者多次任意形式的總數為 300 位元組收到。假設 A 給 B 傳送的 100 位元組和 200 位元組分別都是一個數據包,對於傳送端 A 來說,這個是可以區分的,但是對於 B 來說,如果不人為規定多長為一個數據包,B 每次是不知道應該把收到的資料中多少位元組作為一個有效的資料包的。而規定每次把多少資料當成一個包就是協議格式規範的內容之一。

  經常會有新手寫出類似下面這樣的程式碼:

傳送端:

接收端:

  為了專注問題本身的討論,我這裡省略掉了建立連線和部分錯誤處理的邏輯。上述程式碼中傳送端給接收端傳送了一串字元”the quick brown fox jumps over a lazy dog.“,接收端收到後將其打印出來。

  類似這樣的程式碼在本機一般會工作的很好,接收端也如期打印出來預料的字串,但是一放到區域網或者公網環境就出問題了,即接收端可能打印出來字串並不完整;如果傳送端連續多次傳送字串,接收端會打印出來的字串不完整或出現亂碼。不完整的原因很好理解,即對端某次收到的資料小於完整字串的長度,recvBuf 陣列開始被清空成 0,收到部分字串後,該字串的末尾仍然是 0,printf 函式尋找以 0 為結束標誌的字元結束輸出亂碼的原因是如果某次收入的資料不僅包含一個完整的字串,還包含下一個字串部分內容,那麼 recvBuf 陣列將會被填滿,printf 函式輸出時仍然會尋找以 0 為結束標誌的字元結束輸出,這樣讀取的記憶體就越界了,一直找到為止,而越界後的記憶體可能是一些不可讀字元,顯示出來後就亂碼了


  我舉這個例子希望你明白 能對TCP 協議是流式協議有一個直觀的認識。正因為如此,所以我們需要人為地在傳送端和接收端規定每一次的位元組流邊界,以便接收端知道從什麼位置取出多少位元組來當成一個數據包去解析,這就是我們設計網路通訊協議格式的要做的工作之一

二、如何解決粘包問題

2.1 簡介

  網路通訊程式實際開發中,或者技術面試時,面試官通常會問的比較多的一個問題是:網路通訊時,如何解決粘包?

  有的面試官可能會這麼問:網路通訊時,如何解決粘包、丟包或者包亂序問題?這個問題其實是面試官在考察面試者的網路基礎知識,如果是 TCP 協議,在大多數場景下,是不存在丟包和包亂序問題的,TCP 通訊是可靠通訊方式,TCP 協議棧通過序列號和包重傳確認機制保證資料包的有序和一定被正確發到目的地;如果是 UDP 協議,如果不能接受少量丟包,那就要自己在 UDP 的基礎上實現類似 TCP 這種有序和可靠傳輸機制了(例如 RTP協議、RUDP 協議)。所以,問題拆解後,只剩下如何解決粘包的問題。

2.2 什麼是TCP粘包問題

  TCP粘包就是指傳送方傳送的若干包資料到達接收方時粘成了一包,從接收緩衝區來看,後一包資料的頭緊接著前一包資料的尾,出現粘包的原因是多方面的,可能是來自發送方,也可能是來自接收方

2.3造成TCP粘包的原因

2.3.1傳送方原因

  TCP預設使用Nagle演算法(主要作用:減少網路中報文段的數量),而Nagle演算法主要做兩件事:

    • 1、只有上一個分組得到確認,才會傳送下一個分組
    • 2、收集多個小分組,在一個確認到來時一起傳送

  Nagle演算法造成了傳送方可能會出現粘包問題.

2.3.2 接收方原因

  TCP接收到資料包時,並不會馬上交到應用層進行處理,或者說應用層並不會立即處理。實際上,TCP將接收到的資料包儲存在接收快取裡,然後應用程式主動從快取讀取收到的分組。這樣一來,如果TCP接收資料包到快取的速度大於應用程式從快取中讀取資料包的速度,多個包就會被快取,應用程式就有可能讀取到多個首尾相接粘到一起的包。

2.4 什麼是TCP半包

  接收端收到的資料只是一個包的部分,這種情況一般也叫半包

2.5 如何解決粘包和半包

  無論是半包還是粘包問題,其根源是上文介紹中 TCP 協議是流式資料格式。解決問題的思路還是想辦法從收到的資料中把包與包的邊界給區分出來。那麼如何區分呢?目前主要有三種方法:

2.5.1固定包長的資料包

  顧名思義,即每個協議包的長度都是固定的。舉個例子,例如我們可以規定每個協議包的大小是 64 個位元組,每次收滿 64 個位元組,就取出來解析(如果不夠,就先存起來)。

  這種通訊協議的格式簡單但靈活性差。如果包內容不足指定的位元組數,剩餘的空間需要填充特殊的資訊,如 \0(如果不填充特殊內容,如何區分包裡面的正常內容與填充資訊呢?);如果包內容超過指定位元組數,又得分包分片,需要增加額外處理邏輯——在傳送端進行分包分片,在接收端重新組裝包片(分包和分片內容在接下來會詳細介紹)。

2.5.2以指定字元(串)為包的結束標誌

  這種協議包比較常見,即位元組流中遇到特殊的符號值時就認為到一個包的末尾了。例如,我們熟悉的 FTP協議,發郵件的 SMTP 協議,一個命令或者一段資料後面加上"\r\n"(即所謂的 CRLF)表示一個包的結束。對端收到後,每遇到一個”\r\n“就把之前的資料當做一個數據包。

  這種協議一般用於一些包含各種命令控制的應用中,其不足之處就是如果協議資料包內容部分需要使用包結束標誌字元,就需要對這些字元做轉碼或者轉義操作,以免被接收方錯誤地當成包結束標誌而誤解析

2.5.3包頭 + 包體格式

  這種格式的包一般分為兩部分,即包頭和包體,包頭是固定大小的,且包頭中必須含有一個欄位來說明接下來的包體有多大。

1 struct msg_header
2 {
3   int32_t bodySize;
4   int32_t cmd;
5 };

  這就是一個典型的包頭格式,bodySize 指定了這個包的包體是多大。由於包頭大小是固定的(這裡是 size(int32_t) + sizeof(int32_t) = 8 位元組),對端先收取包頭大小位元組數目(當然,如果不夠還是先快取起來,直到收夠為止),然後解析包頭,根據包頭中指定的包體大小來收取包體,等包體收夠了,就組裝成一個完整的包來處理。在有些實現中,包頭中的 bodySize可能被另外一個叫 packageSize 的欄位代替,這個欄位的含義是整個包的大小,這個時候,我們只要用 packageSize 減去包頭大小(這裡是 sizeof(msg_header))就能算出包體的大小,原理同上。

  在使用大多數網路庫時,通常你需要根據協議格式自己給資料包分界和解析,一般的網路庫不提供這種功能是出於需要支援不同的協議,由於協議的不確定性,因此沒法預先提供具體解包程式碼。當然,這不是絕對的,也有一些網路庫提供了這種功能。在 Java Netty 網路框架中,提供了FixedLengthFrameDecoder 類去處理長度是定長的協議包,提供了 DelimiterBasedFrameDecoder 類去處理按特殊字元作為結束符的協議包,提供 ByteToMessageDecoder 去處理自定義格式的協議包(可用來處理包頭 + 包體 這種格式的資料包),然而在繼承 ByteToMessageDecoder 子類中你需要根據你的協議具體格式重寫 decode() 方法來對資料包解包。

2.5.4 包頭 + 包體格式方案解包處理流程

  在理解了前面介紹的資料包的三種格式後,我們來介紹一下針對上述三種格式的資料包技術上應該如何處理。其處理流程都是一樣的,這裡我們以包頭 + 包體這種格式的資料包來說明。處理流程如下:

三、UDP會不會產生粘包問題

  TCP為了保證可靠傳輸並減少額外的開銷(每次發包都要驗證),採用了基於流的傳輸,基於位元組流的傳輸不認為訊息是一條一條的,是無保護訊息邊界的(保護訊息邊界:指傳輸協議把資料當做一條獨立的訊息在網上傳輸,接收端一次只能接受一條獨立的訊息)。

  UDP則是面向訊息傳輸的,是有保護訊息邊界的,接收方一次只接受一條獨立的資訊,而且UDP傳送方不會使用塊的合併優化演算法,這樣,實際上目前認為,是由於UDP支援的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊),這樣,對於接收端來說,就容易進行區分處理了。所以UDP不會出現粘包問題,所以不存在粘包問題。

  舉個例子:有三個資料包,大小分別為2k、4k、6k,如果採用UDP傳送的話,不管接受方的接收快取有多大,我們必須要進行至少三次以上的傳送才能把資料包傳送完,但是使用TCP協議傳送的話,我們只需要接受方的接收快取有12k的大小,就可以一次把這3個數據包全部發送完畢。

四、轉載於

  https://blog.csdn.net/weixin_41563161/article/details/105164346

本文來自部落格園,作者:Mr-xxx,轉載請註明原文連結:https://www.cnblogs.com/MrLiuZF/p/15154433.html