1. 程式人生 > >TCP的流式服務與拆包粘包

TCP的流式服務與拆包粘包

TCP的流式服務

客戶端傳送位元組流時,tcp會保證伺服器端按照順序接受到全部的位元組流,其他諸如資料包的大小等,tcp協議對我們來說是透明的,我們可以完全不考慮。我們傳送資料和接受資料只用send或者recv函式,只需要關注其返回值,直到傳送了多少資料或者接收到多少資料。其他的通通不管,在TCP通訊中,我們也不需要關心資料包的大小,個數,我們只需要在客戶端建立一個緩衝區不斷髮送,在伺服器端建立一個緩衝區不斷的接收就ok了,當然我們也可以定義一個包頭,來實現傳送檔案的強大功能。

位元組流服務:傳送端send()只是將資料寫到TCP傳送緩衝區中,然後將傳送緩衝區中的資料打包成報文段傳送出去。接收端又將接收到的報文段寫到緩衝區中,最後recv()直接取資料。

位元組流服務特點:資料沒有明確分割(由底層做分割),不分一定的報文段,什麼時候想發便可將寫入緩衝區的資料,進行打包再發送,即send()與recv()的次數沒有必然聯絡。

資料報服務:傳送端sendto()將資料直接打包成相對應的報文段傳送。
資料報服務特點:資料有明確分割,拿資料按報文段拿。

Tcp的封裝過程:

 

連續send兩次,對方會接受幾次

當每個socket建立後會有一個傳送緩衝區和一個接收緩衝區,windows系統預設是8KB,send呼叫成功以後資料並沒有立即發出去,而只是把傳送的資料複製到傳送緩衝區,由作業系統底層實現傳送功能,傳送到接受端的接收緩衝區。為了減輕網路負擔,一般的TCP連結用了nagle演算法,並不是傳送緩衝區有資料就會發送的。 
   對於接收端來說,receive(char *buf,num)只是從接收緩衝區裡面取資料,返回的值就是取得的資料大小。你多次send,如果資料量不大,而num的值超過了傳送的總值,那麼就會一次取完接受緩衝區的資料。

tcp 收發緩衝區值的設定
[[email protected] www.linuxidc.com]# cat /proc/sys/net/ipv4/tcp_rmem  
4096    87380   4161536
tcp接收緩衝區的預設值87380   最小值為4096   最大值為4161536
[[email protected] www.linuxidc.com]# cat /proc/sys/net/ipv4/tcp_wmem 
4096    16384   4161536
tcp 傳送緩衝區的預設值16384   最小值為4096   最大值為4161536

粘包、拆包發生原因

發生TCP粘包或拆包有很多原因
1、要傳送的資料大於TCP傳送緩衝區剩餘空間大小,將會發生拆包。
2、待發送資料大於MSS(最大報文長度),TCP在傳輸前將進行拆包。
3、要傳送的資料小於TCP傳送緩衝區的大小,TCP將多次寫入緩衝區的資料一次傳送出去,將會發生粘包。
4、接收資料端的應用層沒有及時讀取接收緩衝區中的資料,將發生粘包。

為什麼會出現粘包?

由Nagle(內高)演算法造成的傳送端的粘包:Nagle演算法是一種改善網路傳輸效率的演算法.簡單的說,當我們send一段資料給TCP傳送時,TCP並不立刻傳送此段資料,而是等待一小段時間,看看在等待期間是否還有要傳送的資料,若有則會一次把這兩段資料傳送出去.這是對Nagle演算法一個簡單的解釋

為什麼TCP會有粘包拆包的問題而UDP沒有

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

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

粘包、拆包解決辦法
解決問題的關鍵在於如何給每個資料包新增邊界資訊,常用的方法有如下幾個:

1、傳送端給每個資料包新增包首部,首部中應該至少包含資料包的長度,這樣接收端在接收到資料後,通過讀取包首部的長度欄位,便知道每一個數據包的實際長度了。
2、傳送端將每個資料包封裝為
固定長度(不夠的可以通過補0填充),樣接收端每次從接收緩衝區中讀取固定長度的資料就自然而然的把每個資料包拆分開來。
3、可以在資料包之間設定邊界,如
新增特殊符號,這樣,接收端通過這個邊界就可以將不同的資料包拆分開。
等等。

測試的時候採用的是一臺機器連續傳送資料來模擬高併發的場景,所以在測試的時候會發現伺服器端收到的資料包的個數經常會小於包的序號,好像發生了丟包。但經過仔細分析可以發現,這種情況是因為TCP傳送快取溢位導致的丟包,也就是這個資料包根本沒有發出來。也就是說,傳送端傳送資料過快,導致接收端快取很快被填滿,這個時候接收端會把通知視窗設定為0從而控制傳送端的流量,這樣新到的資料只能暫存在傳送端的傳送快取中,當傳送快取溢位後,就出現了我上面提到的丟包,這個問題可以通過增大發送端快取來緩解這個問題。

 

既然保護訊息邊界和流是重點,那麼什麼是保護訊息邊界和流

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

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

注意:

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

結論:

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

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

 

怎樣封包和拆包

封包

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

拆包

對於拆包目前我最常用的方式:利用底層的緩衝區來進行拆包

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

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

程式設計實現見:http://blog.csdn.net/zhangxinrun/article/details/6721495

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

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

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

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

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

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

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

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

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

如何解決拆包粘包

既然知道了tcp是無界的資料流,且協議本身無法避免粘包,拆包的發生,那我們只能在應用層資料協議上,加以控制。通常在制定傳輸資料時,可以使用如下方法:

  1. 使用帶訊息頭的協議、訊息頭儲存訊息開始標識及訊息長度資訊,服務端獲取訊息頭的時候解析出訊息長度,然後向後讀取該長度的內容。
  2. 設定定長訊息,服務端每次讀取既定長度的內容作為一條完整訊息。
  3. 設定訊息邊界,服務端從網路流中按訊息編輯分離出訊息內容。

問題雜述:

問題1、粘包問題

解決方法一:TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料傳送出去,而不必等待發送緩衝區滿;

解決方法二:傳送固定長度的訊息

解決方法三:把訊息的尺寸與訊息一塊傳送

解決方法四:雙方約定每次傳送的大小

解決方法五:雙方約定使用特殊標記來區分訊息間隔

解決方法六:標準協議按協議規則處理,如Sip協議

問題2、字串編碼問題

將中文字串用utf8編碼格式轉換為位元組陣列傳送時,一箇中文字元可能會佔用2~4個位元組(假設為3個位元組),這3個位元組可能分3次接收,接收端每次接收完後用utf8編碼格式轉換為字串,就會出現亂碼,並導致接收長度計算錯誤的情況。

解決方法一:以位元組數做為訊息長度的計算單位,而不是字元個數。

解決方法二:傳送方和接收方都採用unicode編碼格式。

問題3、長連線的保活問題

標準TCP層協議裡把對方超時設為2小時,若伺服器端超過了2小時還沒收到客戶的資訊,它就傳送探測報文段,若傳送了10個探測報文段(每一個相隔75S)還沒有收到響應,就假定客戶出了故障,並終止這個連線。因此應對tcp長連線進行保活。

以下是非同步通訊時會遇到的問題:

問題4、緩衝區髒資料問題

同步傳送的拷貝,是直接拷貝資料到基礎系統緩衝區,拷貝完成後返回;

非同步傳送訊息的拷貝,是將Socket自帶的Buffer空間內的所有資料,拷貝到基礎系統傳送緩衝區,並立即返回;

因此非同步傳送時緩衝區設定不好會導致接收到髒資料的問題,如下所示:

第一次傳送資料:1234567890

第一次接受資料:1234567890

第二次傳送資料:abc

第二次接受資料:abc4567890

請參考:http://www.cnblogs.com/tianzhiliang/archive/2010/09/08/1821623.html

解決方法一:將緩衝區的大小設定為實際傳送資料的大小。

問題5、記憶體碎片問題

頻繁的申請緩衝區會導致記憶體碎片的問題。

解決方法一:使用物件池和記憶體池。

請參考MSDN:http://msdn.microsoft.com/zh-cn/library/bb517542(v=vs.100).aspx

http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.socketasynceventargs(v=vs.100).aspx

問題6、亂序問題

多個執行緒使用非同步通訊方式向同一個接收端(socket)同時傳送資料,會導致接收端接收的資料混亂。如下所示:

執行緒1第一次傳送:123456789,假設未傳送完,只發送了123

執行緒2第一次傳送:abcdefgh,假設未傳送完,只發送了abc

執行緒1第二次傳送:456789,傳送完成

執行緒2第二次傳送:defgh,傳送完成

接收端最終接收的資料為:123abc456789defgh。

解決方法一:一個連線的傳送端執行緒排隊傳送資料。

https://blog.csdn.net/zhangxinrun/article/details/6721495

 

參考:http://blog.csdn.net/zhangxinrun/article/details/6721427