1. 程式人生 > >TCP通訊粘包問題分析和解決(全)

TCP通訊粘包問題分析和解決(全)

在socket網路程式中,TCP和UDP分別是面向連線和非面向連線的。因此TCP的socket程式設計,收發兩端(客戶端和伺服器端)都要有成對的socket,因此,傳送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小、資料量小的資料,合併成一個大的資料塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。

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

====================================================================

在介紹TCP之前先普及下兩個相關的概念,長連線和短連線。

1.長連線

Client方與Server方先建立通訊連線,連線建立後 不斷開, 然後再進行報文傳送和接收。

2.短連線

Client方與Server每進行一次報文收發交易時才進行通訊連線,交易完畢後立即斷開連線。此種方式常用於一點對多點通訊,比如多個Client連線一個Server.

TCP協議簡介

TCP是一個面向連線的傳輸層協議,雖然TCP不屬於ISO制定的協議集,但由於其在商業界和工業界的成功應用,它已成為事實上的網路標準,廣泛應用於各種網路主機間的通訊。

作為一個面向連線的傳輸層協議,TCP的目標是為使用者提供可靠的端到端連線,保證資訊有序無誤的傳輸。它除了提供基本的資料傳輸功能外,還為保證可靠性採用了資料編號、校驗和計算、資料確認等一系列措施。它對傳送的每個資料位元組都進行編號,並請求接收方回傳確認資訊(ACK)。傳送方如果在規定的時間內沒有收到資料確認,就重傳該資料。

(1)     資料編號使接收方能夠處理資料的失序和重複問題。

(2)     資料誤碼問題通過在每個傳輸的資料段中增加校驗和予以解決,接收方在接收到資料後檢查校驗和,若校驗和有誤,則丟棄該有誤碼的資料段,並要求傳送方重傳。

(3)     流量控制也是保證可靠性的一個重要措施,若無流控,可能會因接收緩衝區溢位而丟失大量資料,導致許多重傳,造成網路擁塞惡性迴圈。

(4)     TCP採用可變視窗進行流量控制,由接收方控制傳送方傳送的資料量。

TCP為使用者提供了高可靠性的網路傳輸服務,但可靠性保障措施也影響了傳輸效率。因此,在實際工程應用中,只有關鍵資料的傳輸才採用TCP,而普通資料的傳輸一般採用高效率的UDP。

保護訊息邊界和流

那麼什麼是保護訊息邊界和流呢?

保護訊息邊界,就是指傳輸協議把資料當作一條獨立的訊息在網上傳輸,接收端只能接收獨立的訊息。也就是說存在保護訊息邊界,接收端一次只能接收發送端發出的一個數據包。而面向流則是指無保護訊息保護邊界的,如果傳送端連續傳送資料,接收端有可能在一次接收動作中,會接收兩個或者更多的資料包。

例如,我們連續傳送三個資料包,大小分別是2k,4k ,8k,這三個資料包,都已經到達了接收端的網路堆疊中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收資料,我們必須有三次接收動作,才能夠把所有的資料包接收完.而使用TCP協議,我們只要把接收的緩衝區大小設定在14k以上,我們就能夠一次把所有的資料包接收下來,只需要有一次接收動作。

注意:

這就是因為UDP協議的保護訊息邊界使得每一個訊息都是獨立的。而流傳輸卻把資料當作一串資料流,他不認為資料是一個一個的訊息。所以有很多人在使用tcp協議通訊的時候,並不清楚tcp是基於流的傳輸,當連續傳送資料的時候,他們時常會認識tcp會丟包。其實不然,因為當他們使用的緩衝區足夠大時,他們有可能會一次接收到兩個甚至更多的資料包,而很多人往往會忽視這一點,只解析檢查了第一個資料包,而已經接收的其他資料包卻被忽略了。所以大家如果要作這類的網路程式設計的時候,必須要注意這一點。

結論:

(1)TCP為了保證可靠傳輸,儘量減少額外開銷(每次發包都要驗證),因此採用了流式傳輸,面向流的傳輸,相對於面向訊息的傳輸,可以減少傳送包的數量,從而減少了額外開銷。但是,對於資料傳輸頻繁的程式來講,使用TCP可能會容易粘包。當然,對接收端的程式來講,如果機器負荷很重,也會在接收緩衝裡粘包。這樣,就需要接收端額外拆包,增加了工作量。因此,這個特別適合的是資料要求可靠傳輸,但是不需要太頻繁傳輸的場合(兩次操作間隔100ms,具體是由TCP等待發送間隔決定的,取決於核心中的socket的寫法)

(2)UDP,由於面向的是訊息傳輸,它把所有接收到的訊息都掛接到緩衝區的接受佇列中,因此,它對於資料的提取分離就更加方便,但是,它沒有粘包機制,因此,當傳送資料量較小的時候,就會發生資料包有效載荷較小的情況,也會增加多次傳送的系統傳送開銷(系統呼叫,寫硬體等)和接收開銷。因此,應該最好設定一個比較合適的資料包的包長,來進行UDP資料的傳送。(UDP最大載荷為1472,因此最好能每次傳輸接近這個數的資料量,這特別適合於視訊,音訊等大塊資料的傳送,同時,通過減少握手來保證流媒體的實時性

====================================================================

粘包問題分析與對策

TCP粘包是指傳送方傳送的若干包資料到接收方接收時粘成一包,從接收緩衝區看,後一包資料的頭緊接著前一包資料的尾。

出現粘包現象的原因是多方面的,它既可能由傳送方造成,也可能由接收方造成。

什麼時候需要考慮粘包問題

1如果利用tcp每次傳送資料,就與對方建立連線,然後雙方傳送完一段資料後,就關閉連線,這樣就不會出現粘包問題(因為只有一種包結構,類似於http協議)。

關閉連線主要是要雙方都發送close連線(參考tcp關閉協議)。如:A需要傳送一段字串給B,那麼A與B建立連線,然後傳送雙方都預設好的協議字元如"hello give me sth abour yourself",然後B收到報文後,就將緩衝區資料接收,然後關閉連線,這樣粘包問題不用考慮到,因為大家都知道是傳送一段字元。

2如果傳送資料無結構,如檔案傳輸,這樣傳送方只管傳送,接收方只管接收儲存就ok,也不用考慮粘包3如果雙方建立連線,需要在連線後一段時間內傳送不同結構資料,如連線後,有好幾種結構:

1)"hellogive me sth abour yourself"

2)"Don'tgive me sth abour yourself"

那這樣的話,如果傳送方連續傳送這個兩個包出去,接收方一次接收可能會是"hellogive me sth abour yourselfDon't give me sth abour yourself"這樣接收方就傻了,到底是要幹嘛?不知道,因為協議沒有規定這麼詭異的字串,所以要處理把它分包,怎麼分也需要雙方組織一個比較好的包結構,所以一般可能會在頭加一個數據長度之類的包,以確保接收。

粘包出現原因

簡單得說,在流傳輸中出現,UDP不會出現粘包,因為它有訊息邊界(參考Windows網路程式設計)

1傳送端需要等緩衝區滿才傳送出去,造成粘包

2接收方不及時接收緩衝區的包,造成多個包接收

具體點:

(1)傳送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,傳送方往往要收集到足夠多的資料後才傳送一包資料。若連續幾次傳送的資料都很少,通常TCP會根據優化演算法把這些資料合成一包後一次傳送出去,這樣接收方就收到了粘包資料。

(2)接收方引起的粘包是由於接收方使用者程序不及時接收資料,從而導致粘包現象。這是因為接收方先把收到的資料放在系統接收緩衝區,使用者程序從該緩衝區取資料,若下一包資料到達時前一包資料尚未被使用者程序取走,則下一包資料放到系統接收緩衝區時就接到前一包資料之後,而使用者程序根據預先設定的緩衝區大小從系統接收緩衝區取資料,這樣就一次取到了多包資料。

粘包情況有兩種,一種是粘在一起的包都是完整的資料包,另一種情況是粘在一起的包有不完整的包。

不是所有的粘包現象都需要處理,若傳輸的資料為不帶結構的連續流資料(如檔案傳輸),則不必把粘連的包分開(簡稱分包)。但在實際工程應用中,傳輸的資料一般為帶結構的資料,這時就需要做分包處理。

在處理定長結構資料的粘包問題時,分包演算法比較簡單;在處理不定長結構資料的粘包問題時,分包演算法就比較複雜。特別是粘在一起的包有不完整的包的粘包情況,由於一包資料內容被分在了兩個連續的接收包中,處理起來難度較大。實際工程應用中應儘量避免出現粘包現象。

為了避免粘包現象,可採取以下幾種措施:

(1)對於傳送方引起的粘包現象,使用者可通過程式設計設定來避免,TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料傳送出去,而不必等待發送緩衝區滿;

(2)對於接收方引起的粘包,則可通過優化程式設計、精簡接收程序工作量、提高接收程序優先順序等措施,使其及時接收資料,從而儘量避免出現粘包現象;

(3)由接收方控制,將一包資料按結構欄位,人為控制分多次接收,然後合併,通過這種手段來避免粘包。

以上提到的三種措施,都有其不足之處。

(1)第一種程式設計設定方法雖然可以避免傳送方引起的粘包,但它關閉了優化演算法,降低了網路傳送效率,影響應用程式的效能,一般不建議使用。

(2)第二種方法只能減少出現粘包的可能性,但並不能完全避免粘包,當傳送頻率較高時,或由於網路突發可能使某個時間段資料包到達接收方較快,接收方還是有可能來不及接收,從而導致粘包。

(3)第三種方法雖然避免了粘包,但應用程式的效率較低,對實時應用的場合不適合。

一種比較周全的對策是:接收方建立一預處理執行緒,對接收到的資料包進行預處理,將粘連的包分開。對這種方法我們進行了實驗,證明是高效可行的。

TCP無保護訊息邊界的解決

針對這個問題,一般有3種解決方案:

(1)傳送固定長度的訊息

(2)把訊息的尺寸與訊息一塊傳送

(3)使用特殊標記來區分訊息間隔

====================================================================

網路通訊的封包和拆包

對於基於TCP開發的通訊程式,有個很重要的問題需要解決,就是封包和拆包。

為什麼基於TCP的通訊程式需要進行封包和拆包

TCP是個"流"協議,所謂流,就是沒有界限的一串資料,大家可以想想河裡的流水,是連成一片的,其間是沒有分界線的。但一般通訊程式開發是需要定義一個個相互獨立的資料包的,比如用於登陸的資料包,用於登出的資料包。由於TCP"流"的特性以及網路狀況,在進行資料傳輸時會出現以下幾種情況。

假設我們連續呼叫兩次send分別傳送兩段資料data1和data2,在接收端有以下幾種接收情況(當然不止這幾種情況,這裡只列出了有代表性的情況).

A.先接收到data1,然後接收到data2.

B.先接收到data1的部分資料,然後接收到data1餘下的部分以及data2的全部.

C.先接收到了data1的全部資料和data2的部分資料,然後接收到了data2的餘下的資料.

D.一次性接收到了data1和data2的全部資料.

對於A這種情況正是我們需要的,不再做討論.對於B,C,D的情況就是大家經常說的"粘包",就需要我們把接收到的資料進行拆包,拆成一個個獨立的資料包,為了拆包就必須在傳送端進行封包。

另:對於UDP來說就不存在拆包的問題,因為UDP是個"資料包"協議,也就是兩段資料間是有界限的,在接收端要麼接收不到資料要麼就是接收一個完整的一段資料,不會少接收也不會多接收。

為什麼會出現B.C.D的情況

1.由Nagle演算法造成的傳送端的粘包:Nagle演算法是一種改善網路傳輸效率的演算法.簡單的說,當我們提交一段資料給TCP傳送時,TCP並不立刻傳送此段資料,而是等待一小段時間,看看在等待期間是否還有要傳送的資料,若有則會一次把這兩段資料傳送出去.這是對Nagle演算法一個簡單的解釋,詳細的請看相關書籍. C和D的情況就有可能是Nagle演算法造成的.

2.接收端接收不及時造成的接收端粘包:TCP會把接收到的資料存在自己的緩衝區中,然後通知應用層取資料.當應用層由於某些原因不能及時的把TCP的資料取出來,就會造成TCP緩衝區中存放了幾段資料.

怎樣封包和拆包

最初遇到"粘包"的問題時,我是通過在兩次send之間呼叫sleep來休眠一小段時間來解決。這個解決方法的缺點是顯而易見的,使傳輸效率大大降低,而且也並不可靠。後來就是通過應答的方式來解決,儘管在大多數時候是可行的,但是不能解決B的那種情況,而且採用應答方式增加了通訊量,加重了網路負荷. 再後來就是對資料包進行封包和拆包的操作。

封包

封包就是給一段資料加上包頭,這樣一來資料包就分為包頭和包體兩部分內容了(以後講過濾非法包時封包會加入"包尾"內容)。包頭其實上是個大小固定的結構體,其中有個結構體成員變量表示包體的長度,這是個很重要的變數,其他的結構體成員可根據需要自己定義。根據包頭長度固定以及包頭中含有包體長度的變數就能正確的拆分出一個完整的資料包。

拆包

對於拆包目前我最常用的是以下兩種方式:

(1)動態緩衝區暫存方式。之所以說緩衝區是動態的是因為當需要緩衝的資料長度超出緩衝區的長度時會增大緩衝區長度。

大概過程描述如下:

A,為每一個連線動態分配一個緩衝區,同時把此緩衝區和SOCKET關聯,常用的是通過結構體關聯.

B,當接收到資料時首先把此段資料存放在緩衝區中.

C,判斷快取區中的資料長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.

D,根據包頭資料解析出裡面代表包體長度的變數.

E,判斷快取區中除包頭外的資料長度是否夠一個包體的長度,如不夠,則不進行拆包操作.

F,取出整個資料包.這裡的"取"的意思是不光從緩衝區中拷貝出資料包,而且要把此資料包從快取區中刪除掉.刪除的辦法就是把此包後面的資料移動到緩衝區的起始地址.

這種方法有兩個缺點.

1) 為每個連線動態分配一個緩衝區增大了記憶體的使用.

2) 有三個地方需要拷貝資料,一個地方是把資料存放在緩衝區,一個地方是把完整的資料包從緩衝區取出來,一個地方是把資料包從緩衝區中刪除.第二種拆包的方法會解決和完善這些缺點.

前面提到過這種方法的缺點.下面給出一個改進辦法, 即採用環形緩衝.但是這種改進方法還是不能解決第一個缺點以及第一個資料拷貝,只能解決第三個地方的資料拷貝(這個地方是拷貝資料最多的地方).第2種拆包方式會解決這兩個問題.

環形緩衝實現方案是定義兩個指標,分別指向有效資料的頭和尾.在存放資料和刪除資料時只是進行頭尾指標的移動.

(2)利用底層的緩衝區來進行拆包

由於TCP也維護了一個緩衝區,所以我們完全可以利用TCP的緩衝區來快取我們的資料,這樣一來就不需要為每一個連線分配一個緩衝區了。另一方面我們知道recv或者wsarecv都有一個引數,用來表示我們要接收多長長度的資料。利用這兩個條件我們就可以對第一種方法進行優化。

對於阻塞SOCKET來說,我們可以利用一個迴圈來接收包頭長度的資料,然後解析出代表包體長度的那個變數,再用一個迴圈來接收包體長度的資料。

這個問題產生於程式設計中遇到的幾個問題:

1、使用TCP的Socket傳送資料的時候,會出現傳送出錯,WSAEWOULDBLOCK,在TCP中不是會保證傳送的資料能夠安全的到達接收端的嗎?也有視窗機制去防止傳送速度過快,為什麼還會出錯呢?

2、TCP協議,在使用Socket傳送資料的時候,每次傳送一個包,接收端是完整的接受到一個包還是怎麼樣?如果是每發一個包,就接受一個包,為什麼還會出現粘包問題,具體是怎麼執行的?

3、關於Send,是不是隻有在非阻塞狀態下才會出現實際傳送的比指定傳送的小?在阻塞狀態下會不會出現實際傳送的比指定傳送的小,就是說只能出現要麼全傳送,要麼不傳送?在非阻塞狀態下,如果之傳送了一些資料,要怎麼處理,呼叫了Send函式後,發現返回值比指定的要小,具體要怎麼做?

4、最後一個問題,就是TCP/IP協議和Socket是什麼關係?是指具體的實現上,Socket是TCP/IP的實現?那麼為什麼會出現使用TCP協議的Socket會發送出錯。

這個問題第1個回答:

1應該是你的緩衝區不夠大,

2 tcp是流,沒有界限.也就沒所謂的包.

3阻塞也會出現這種現象,出現後繼續傳送沒傳送出去的.

4tcp是協議,socket是一種介面,沒必然聯絡.錯誤取決於你使用介面的問題,跟tcp沒關係.

這個問題第2個回答:

1、應該不是緩衝區大小問題,我試過設定緩衝區大小,不過這裡有個問題,就是就算我把緩衝區設定成幾G,也返回成功,不過實際上怎麼可能設定那麼大

3、出現沒傳送完的時候要手動傳送吧,有沒有具體的程式碼實現?

4、當選擇TCP的Socket傳送資料的時候,TCP中的視窗機制不是能防止傳送速度過快的嗎?為什麼Socket在出現了WSAEWOULDBLOCK後沒有處理?

這個問題第3個回答:

1.在使用非阻塞模式的情況下,如果系統傳送緩衝區已滿,並示及時傳送到對端,就會產生該錯誤,繼續重試即可。

3.如果沒有發完就繼續傳送後續部分即可。

這個問題第4個回答:

1、使用非阻塞模式時,如果當前操作不能立即完成則會返回失敗,錯誤碼是WSAEWOULDBLOCK,這是正常的,程式可以先執行其它任務,過一段時間後再重試該操作。

2、傳送與接收不是一一對應的,TCP會把各次傳送的資料重新組合,可能合併也可能拆分,但傳送次序是不變的。

3、在各種情況下都要根據send的返回值來確定傳送了多少資料,沒有傳送完就再接著發。

4、socket是Windows提供網路程式設計介面,TCP/IP是網路傳輸協議,使用socket是可以使用多種協議,其中包括TCP/IP。

這個問題第5個回答:

傳送的過程是:傳送到緩衝區和從緩衝區傳送到網路上

WSAEWOULDBLOCK和粘包都是出現在傳送到緩衝區這個過程的

Socket程式設計 (非同步通訊,解決Tcp粘包)

前面提到,TCP會出現粘包問題,下面將以例項演示解決方案:

問題一般會出現的情況如下,假設我們連續傳送兩條兩天記錄("我是liger_zql"):

模擬傳送示例:

#region 測試訊息傳送,並匹配協議

 TcpClient client =new TcpClient();

 client.AsynConnect();

 Console.WriteLine("下面將連續傳送2條測試訊息...");

 Console.ReadKey();

 MessageProtocol msgPro;

  for (int i = 0; i<2; i++)

  {

     msgPro =newMessageProtocol("我是liger_zql");

     Console.WriteLine("第{0}條:{1}", i +1,msgPro.MessageInfo.Content);

     client.AsynSend(msgPro);

  }

  #endregion

接收端接受兩條資訊會出現如下三種情況:

1.(1)我是liger_zql(2)我是liger_zql

2.(1)我是liger_zql我是(2)liger_zql

3.(1)我是liger_zql我是liger_zql

通過以上三種情況,顯然2、3都不是我們想要的結果。那麼如何處理這中情況呢?

解決方案:通過自定義協議...

我們可以以將資訊以xml的格式傳送出去,列入<protocol>content</protocol>通過正則匹配資訊是否完整,如果不完整,我們可以先將本次接受資訊快取接受下一次資訊,再次匹配得到相應的結果。

(1)將資訊物件轉換成一定格式的xml字串:

(2)對接收的資訊通過正則進行匹配處理:

(3)將該定義的協議換換成資訊物件,通過物件獲取自己想要的資訊。

結果:

最後執行結果如下

附:

參考: