tcp和upd粘包、拆包、ip分片問題

我們都知道TCP屬於傳輸層的協議,傳輸層除了有TCP協議外還有UDP協議。那麼UDP是否會發生粘包或拆包的現象呢?答案是不會UDP是基於報文發送的,從UDP的幀結構可以看出,在UDP首部採用了16bit來指示UDP數據報文的長度,因此在應用層能很好的將不同的數據報文區分開,從而避免粘包和拆包的問題。而TCP是基於字節流的,雖然應用層和TCP傳輸層之間的數據交互是大小不等的數據塊,但是TCP把這些數據塊僅僅看成一連串無結構的字節流,沒有邊界;另外從TCP的幀結構也可以看出,在TCP的首部沒有表示數據長度的字段,基於上面兩點,在使用TCP傳輸數據時,纔有粘包或者拆包現象發生的可能。

TCP粘包、拆包表現形式

現在假設客戶端向服務端連續發送了兩個數據包,用packet1和packet2來表示,那麼服務端收到的數據可以分爲三種,現列舉如下:

第一種情況,接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論範圍內。normal

第二種情況,接收端只收到一個數據包,由於TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即爲粘包。這種情況由於接收端不知道這兩個數據包的界限,所以對於接收端來說很難處理。one

第三種情況,這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要麼是不完整的,要麼就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對於接收端同樣是不好處理的。half_oneone_half

TCP粘包、拆包發生原因

發生TCP粘包或拆包有很多原因,現列出常見的幾點,可能不全面,歡迎補充,

1、要發送的數據大於TCP發送緩衝區剩餘空間大小,將會發生拆包。

2、待發送數據大於MSS(最大報文長度),TCP在傳輸前將進行拆包。

3、要發送的數據小於TCP發送緩衝區的大小,TCP將多次寫入緩衝區的數據一次發送出去,將會發生粘包。

4、接收數據端的應用層沒有及時讀取接收緩衝區中的數據,將發生粘包。

等等

TCP粘包、拆包解決辦法

通過以上分析,我們清楚了粘包或拆包發生的原因,那麼如何解決這個問題呢?解決問題的關鍵在於如何給每個數據包添加邊界信息,常用的方法有如下幾個:

1、發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據後,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。

2、發送端將每個數據包封裝爲固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩衝區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。

3、可以在數據包之間設置邊界,如添加特殊符號,這樣,接收端通過這個邊界就可以將不同的數據包拆分開。


IP分片

1 IP協議簡單介紹

就個人而言,網絡中,拋開網絡安全加密這些,就只單單討論協議本身,比較難的有三個地方: 

  • IP分片與重組
  • TCP滑動窗口與TCP狀態的改變
  • TCP定時器

其實協議本身根據《TCP/IP詳解卷1》理解起來並不難,但是實現起來就很難:數據的操作,標誌位的設置,網絡狀態的變換,中斷多線程通訊等等;

在下圖的七層網絡協議參考模型中,IP層屬於網絡層,網絡層最主要的作用就是:將指定IP的數據報傳輸到對應的主機。

網絡七層參考模型

下圖是以太幀封裝格式(RFC 894),RFC 894封裝格式也是我們最常用的。

下面做個簡單介紹:

  • 目的地址:6字節,即我們常說的以太網物理地址,這裏是目標主機的物理地址,物理地址爲唯一的。大家可能疑惑,發送網絡數據時只寫了IP地址,並沒有寫目的地址啊。這個是底層協議實現的,根據ARP協議,首先將目的地址設爲全1,然後根據IP地址來獲取目的地址。
  • 源地址:6字節,發數據時自己的物理地址。
  • 類型:2字節,協議類型,比如0x0800代表IP協議,0x0806代表ARP協議等等
  • 數據:802.3標準規定,一個以太幀最少64字節,最大1518字節,那麼去掉6字節目的地址、6字節源地址、2字節類型、4字節CRC,對應區間即爲48~1500字節,如果不足48字節可以填充。
  • CRC:顧名思義,校驗部分,所以網絡數據是很準確的,而且絕大多數都支持硬件CRC校驗,速度非常快,幾乎不會在這上面消耗時間。

下面再看看數據部分:IP數據報

IP數據報封裝

圖中從左到右爲0~31位,共四個字節,從上到下依次增長,IP頭部佔20字節,剩下的爲數據,如果傳輸層爲TCP則還有20字節的TCP頭部,如果是UDP則還有8字節(如果分片的話,中間的包沒有UDP頭部,即0字節)的UDP頭部。剩下的纔是真正的用戶要傳送的數據。可以看出傳送同樣多的數據,UDP協議要比TCP傳送的數據多,但從這一點來說UDP速度也要比TCP快。

下面對一些字段做個簡單介紹:

  • 4位版本:比如IPv4、IPv6
  • 4位手部長度:指的是首部佔32 bit字的數目,包括任何選項。由於它是一個4比特字段,因此首部最長爲60個字節
  • 16位總長度:一共16位,理論最大長度爲65535字節,但是受硬件限制,和其它方面的考慮,大部分路由器或主機支持8192字節。
  • 3位標誌:標識是否IP分片.第一位無用,第二位0:允許分片,1:不允許。第三位0:最後一片,1:後面還有分片
  • 13位片偏移:此分片在原始數據的偏移,用於分片重組,因爲13位,所以支持的最大字節爲8192
  • 8位生存時間TTL:規定網絡數據包在網際層傳輸時,最多可以經過路由器的個數,它的大小一般爲256/64,每經過一個路由值就會減1,當它爲0時,數據會被丟棄,並回傳一個ICMP包來通知發送者。

2 IP分片

從上面的介紹我們知道,一個以太幀最大爲1518字節 (14字節以太首部,20字節IP首部,UDP8/TCP20,因此IP包每次最大爲1500==MTU。去掉協議頭UDP有效數據1472字節,TCP爲1460字節。還有最後的4字節CRC),但是一個IP數據報則可能會有8192字節,超過以太幀的最大限制,那麼這時就需要IP分片,分批進行傳輸。

發送方會在IP層將要發送的數據分成多個數據包分批發送,而接收方則將數據按照順序再從新組織起來,等接收到一個完整的數據報之後,然後再提交給上一層傳輸層。

注意,TCP協議爲可靠的傳輸協議,它避免了IP分片的發生,它會在TCP層對數據進行處理,對數據進行分段(不在詳述),IP分片用的多的在UDP協議

我們知道,協議本身並沒有對數據在各個層中間怎麼傳遞做出要求,比如嵌入式實現和BSD實現就不太一樣,因爲嵌入式內存比較少,數據在層與層之間傳遞時會盡量避免數據拷貝,而只是指針的操作。下面我們以嵌入式中用的比較多的LwIP舉例

LwIP允許的最大IP由如下決定:IP_REASS_MAX_PBUFS決定IP分片允許最大pbuf數量,IP_REASS_MAXAGE分片的生存時間,超過則錯誤並將之前接收的IP分片丟棄。

如果數據大於IP_REASS_MAX_PBUFS則有兩種選擇,一,直接刪除數據返回;二,是刪除生存時間最長的IP分片PBUF,這個通過IP_REASS_FREE_OLDEST來使能。

當爲UDP協議時,如果緩衝區描述符大小小於完整的IP數據包,IP分片數據包到來時,很快將描述符耗盡,後來的IP包由於無緩衝區描述符而丟棄,UDP沒有重傳機制,很可能永遠不會接收到完整的IP分片包。從而大於IP_REASS_MAXAGE出現錯誤,因此緩衝區描述符也應增大以適應IP分片重裝。

TCP發送數據時,將大於MSS的數據分段(segment不叫分片),MSS一般爲1460.所以,TCP數據包不會在IP層分片。

IP頭部有3位標誌字段,標誌是否爲分片包。第一位無用,第二位0:允許分片,1:不允許。第三位0:最後一片,1:後面還有分片。13位offset表示偏移,用於IP重組時數據排序,13位因此支持最大IP數據包爲8192字節。

標準的BSD協議實現如下圖所示,採用兩個結構體,IPQ爲表頭,將各個IP分片表頭連接起來,並存儲IP信息。Ipasfrag爲具體的分片數據。

BSD實現


TCP、UDP數據包大小的限制


1、概述

首先要看TCP/IP協議,涉及到四層:鏈路層,網絡層,傳輸層,應用層。   
其中以太網(Ethernet)的數據幀在鏈路層   
IP包在網絡層   
TCP或UDP包在傳輸層   
TCP或UDP中的數據(Data)在應用層   
它們的關係是 數據幀{IP包{TCP或UDP包{Data}}}   

    不同的協議層對數據包有不同的稱謂,在傳輸層叫做段(segment),在網絡層叫做數據報(datagram),在鏈路層叫做幀(frame)。數據封裝成幀後發到傳輸介質上,到達目的主機後每層協議再剝掉相應的首部,最後將應用層數據交給應用程序處理。


在應用程序中我們用到的Data的長度最大是多少,直接取決於底層的限制。   
我們從下到上分析一下:   
1.在鏈路層,由以太網的物理特性決定了數據幀的長度爲(46+18)-(1500+18),其中的18是數據幀的頭和尾,也就是說數據幀的內容最大爲1500(不包括幀頭和幀尾),即MTU(Maximum Transmission Unit)爲1500;  
2.在網絡層,因爲IP包的首部要佔用20字節,所以這的MTU爲1500-20=1480; 
3.在傳輸層,對於UDP包的首部要佔用8字節,所以這的MTU爲1480-8=1472;   
所以,在應用層,你的Data最大長度爲1472。當我們的UDP包中的數據多於MTU(1472)時,發送方的IP層需要分片fragmentation進行傳輸,而在接收方IP層則需要進行數據報重組,由於UDP是不可靠的傳輸協議,如果分片丟失導致重組失敗,將導致UDP數據包被丟棄。   
從上面的分析來看,在普通的局域網環境下,UDP的數據最大爲1472字節最好(避免分片重組)。   
但在網絡編程中,Internet中的路由器可能有設置成不同的值(小於默認值),Internet上的標準MTU值爲576,所以Internet的UDP編程時數據長度最好在576-20-8=548字節以內。
 

2、TCP、UDP數據包最大值的確定     

        UDP和TCP協議利用端口號實現多項應用同時發送和接收數據。數據通過源端口發送出去,通過目標端口接收。有的網絡應用只能使用預留或註冊的靜態端口;而另外一些網絡應用則可以使用未被註冊的動態端口。因爲UDP和TCP報頭使用兩個字節存放端口號,所以端口號的有效範圍是從0到65535。動態端口的範圍是從1024到65535。  

        MTU最大傳輸單元,這個最大傳輸單元實際上和鏈路層協議有着密切的關係,EthernetII幀的結構DMAC+SMAC+Type+Data+CRC由於以太網傳輸電氣方面的限制,每個以太網幀都有最小的大小64Bytes最大不能超過1518Bytes,對於小於或者大於這個限制的以太網幀我們都可以視之爲錯誤的數據幀,一般的以太網轉發設備會丟棄這些數據幀。


        由於以太網EthernetII最大的數據幀是1518Bytes這樣,刨去以太網幀的幀頭(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和幀尾CRC校驗部分4Bytes那麼剩下承載上層協議的地方也就是Data域最大就只能有1500Bytes這個值我們就把它稱之爲MTU。


UDP 包的大小就應該是 1500 - IP頭(20) - UDP頭(8) = 1472(Bytes)
TCP 包的大小就應該是 1500 - IP頭(20) - TCP頭(20) = 1460 (Bytes)


注*PPPoE所謂PPPoE就是在以太網上面跑“PPP”。隨着寬帶接入(這種寬帶接入一般爲Cable Modem或者xDSL或者以太網的接入),因爲以太網缺乏認證計費機制而傳統運營商是通過PPP協議來對撥號等接入服務進行認證計費的,所以引入PPPoE。PPPoE導致MTU變小了以太網的MTU是1500,再減去PPP的包頭包尾的開銷(8Bytes),就變成1492。不過目前大多數的路由設備的MTU都爲1500。


        如果我們定義的TCP和UDP包沒有超過範圍,那麼我們的包在IP層就不用分包了,這樣傳輸過程中就避免了在IP層組包發生的錯誤;如果超過範圍,既IP數據報大於1500字節,發送方IP層就需要將數據包分成若干片,而接收方IP層就需要進行數據報的重組。更嚴重的是,如果使用UDP協議,當IP層組包發生錯誤,那麼包就會被丟棄。接收方無法重組數據報,將導致丟棄整個IP數據報。UDP不保證可靠傳輸;但是TCP發生組包錯誤時,該包會被重傳,保證可靠傳輸。

        UDP數據報的長度是指包括報頭和數據部分在內的總字節數,其中報頭長度固定,數據部分可變。數據報的最大長度根據操作環境的不同而各異。從理論上說,包含報頭在內的數據報的最大長度爲65535字節(64K)。

       我們在用Socket編程時,UDP協議要求包小於64K。TCP沒有限定,TCP包頭中就沒有“包長度”字段,而完全依靠IP層去處理分幀。這就是爲什麼TCP常常被稱作一種“流協議”的原因,開發者在使用TCP服務的時候,不必去關心數據包的大小,只需講SOCKET看作一條數據流的入口,往裏面放數據就是了,TCP協議本身會進行擁塞/流量控制。 

       不過鑑於Internet(非局域網)上的標準MTU值爲576字節,所以建議在進行Internet的UDP編程時,最好將UDP的數據長度控制在548字節 (576-8-20)以內。


3、TCP、UDP數據包最小值的確定

     在用UDP局域網通信時,經常發生“Hello World”來進行測試,但是“Hello World”並不滿足最小有效數據(64-46)的要求,爲什麼小於18個字節,對方仍然可用收到呢?因爲在鏈路層的MAC子層中會進行數據補齊,不足18個字節的用0補齊。但當服務器在公網,客戶端在內網,發生小於18個字節的數據,就會出現接收端收不到數據的情況。

       以太網EthernetII規定,以太網幀數據域部分最小爲46字節,也就是以太網幀最小是6+6+2+46+4=64。除去4個字節的FCS,因此,抓包時就是60字節。當數據字段的長度小於46字節時,MAC子層就會在數據字段的後面填充以滿足數據幀長不小於64字節。由於填充數據是由MAC子層負責,也就是設備驅動程序。不同的抓包程序和設備驅動程序所處的優先層次可能不同,抓包程序的優先級可能比設備驅動程序更高,也就是說,我們的抓包程序可能在設備驅動程序還沒有填充不到64字節的幀的時候,抓包程序已經捕獲了數據。因此不同的抓包工具抓到的數據幀的大小可能不同。下列是本人分別用wireshark和sniffer抓包的結果,對於TCP 的ACK確認幀的大小一個是54字節,一個是60字節,wireshark抓取時沒有填充數據段,sniffer抓取時有填充數據段。

       

4、實際應用

        用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) - UDP頭(8)=65507字節。用sendto函數發送數據時,如果發送數據長度大於該值,則函數會返回錯誤。  

        用TCP協議發送時,由於TCP是數據流協議,因此不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不一定會一次性發送出去,如果這段數據比較長,會被分段發送,如果比較短,可能會等待和下一次數據一起發送。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章