天天看點

關于TCP封包、粘包、半包關于Tcp封包

很多朋友已經對此作了不少研究,也花費不少心血編寫了實作代碼和blog文檔。當然也充斥着一些各式的評論,自己看了一下,總結一些心得。

首先我們學習一下這些朋友的心得,他們是:

<a href="http://blog.csdn.net/stamhe/article/details/4569530">http://blog.csdn.net/stamhe/article/details/4569530</a>

<a href="http://www.cppblog.com/tx7do/archive/2011/05/04/145699.html">http://www.cppblog.com/tx7do/archive/2011/05/04/145699.html</a>

//………………

當然還有太多,很多東西粘來粘區也不知道到底是誰的原作,J

看這些朋友的blog是我建議親自看一下TCP-IP詳解卷1中的相關内容【原理性的内容一定要看】。

TCP-IP詳解卷1第17章中17.2節對TCP服務原理作了一個簡明介紹(以下藍色字型摘自《TCP-IP詳解卷1第17章17.2節》):

盡管T C P和U D P都使用相同的網絡層( I P),T C P卻向應用層提供與U D P完全不同的服務。T C P提供一種面向連接配接的、可靠的位元組流服務。

面向連接配接意味着兩個使用T C P的應用(通常是一個客戶和一個伺服器)在彼此交換資料之前必須先建立一個T C P連接配接。這一過程與打電話很相似,先撥号振鈴,等待對方摘機說“喂”,然後才說明是誰。在第1 8章我們将看到一個T C P連接配接是如何建立的,以及當一方通信結束後如何斷開連接配接。

在一個T C P連接配接中,僅有兩方進行彼此通信。在第1 2章介紹的廣播和多點傳播不能用于T C P。

T C P通過下列方式來提供可靠性:

• 應用資料被分割成T C P認為最适合發送的資料塊。這和U D P完全不同,應用程式産生的資料報長度将保持不變。由T C P傳遞給I P的資訊機關稱為封包段或段( s e g m e n t)(參見圖1 - 7)。在1 8 . 4節我們将看到T C P如何确定封包段的長度。

• 當T C P發出一個段後,它啟動一個定時器,等待目的端确認收到這個封包段。如果不能及時收到一個确認,将重發這個封包段。在第2 1章我們将了解T C P協定中自适應的逾時及重傳政策。

• 當T C P收到發自T C P連接配接另一端的資料,它将發送一個确認。這個确認不是立即發送,通常将推遲幾分之一秒,這将在1 9 . 3節讨論。

• T C P将保持它首部和資料的檢驗和。這是一個端到端的檢驗和,目的是檢測資料在傳輸過程中的任何變化。如果收到段的檢驗和有差錯, T C P将丢棄這個封包段和不确認收到此封包段(希望發端逾時并重發)。

• 既然T C P封包段作為I P資料報來傳輸,而I P資料報的到達可能會失序,是以T C P封包段的到達也可能會失序。如果必要, T C P将對收到的資料進行重新排序,将收到的資料以正确的順序交給應用層。

• 既然I P資料報會發生重複, T C P的接收端必須丢棄重複的資料。

• T C P還能提供流量控制。T C P連接配接的每一方都有固定大小的緩沖空間。T C P的接收端隻允許另一端發送接收端緩沖區所能接納的資料。這将防止較快主機緻使較慢主機的緩沖區溢出。兩個應用程式通過T C P連接配接交換8 bit位元組構成的位元組流。T C P不在位元組流中插入記錄辨別符。我們将這稱為位元組流服務( byte stream service)。如果一方的應用程式先傳1 0位元組,又傳2 0位元組,再傳5 0位元組,連接配接的另一方将無法了解發方每次發送了多少位元組。收方可以分4次接收這8 0個位元組,每次接收2 0位元組。一端将位元組流放到T C P連接配接上,同樣的位元組流将出現在T C P連接配接的另一端。另外,T C P對位元組流的内容不作任何解釋。T C P不知道傳輸的資料位元組流是二進制資料,還是A S C I I字元、E B C D I C字元或者其他類型資料。對位元組流的解釋由T C P連接配接雙方的應用層解釋。這種對位元組流的處理方式與U n i x作業系統對檔案的處理方式很相似。U n i x的核心對一個應用讀或寫的内容不作任何解釋,而是交給應用程式處理。對U n i x的核心來說,它無法區分一個二進制檔案與一個文本檔案。

       我仍然引用官方解釋《TCP-IP詳解卷1》第18章18.4節:

最大封包段長度( M S S)表示T C P傳往另一端的最大塊資料的長度。當一個連接配接建立時【三次握手】,連接配接的雙方都要通告各自的M S S。我們已經見過M S S都是1 0 2 4。這導緻I P資料報通常是4 0位元組長:2 0位元組的T C P首部和2 0位元組的I P首部。

在有些書中,将它看作可“協商”選項。它并不是任何條件下都可協商。當建立一個連

接時,每一方都有用于通告它期望接收的M S S選項(M S S選項隻能出現在S Y N封包段中)。如果一方不接收來自另一方的M S S值,則M S S就定為預設值5 3 6位元組(這個預設值允許2 0位元組的I P首部和2 0位元組的T C P首部以适合5 7 6位元組I P資料報)。

一般說來,如果沒有分段發生, M S S還是越大越好(這也并不總是正确,參見圖2 4 - 3和圖2 4 - 4中的例子)。封包段越大允許每個封包段傳送的資料就越多,相對I P和T C P首部有更高的網絡使用率。當T C P發送一個S Y N時,或者是因為一個本地應用程序想發起一個連接配接,或者是因為另一端的主機收到了一個連接配接請求,它能将M S S值設定為外出接口上的M T U長度減去固定的I P首部和T C P首部長度。對于一個以太網, M S S值可達1 4 6 0位元組。使用IEEE 802.3的封裝(參見2 . 2節),它的M S S可達1 4 5 2位元組。

如果目的I P位址為“非本地的( n o n l o c a l )”,M S S通常的預設值為5 3 6。而區分位址是本地還是非本地是簡單的,如果目的I P位址的網絡号與子網号都和我們的相同,則是本地的;如果目的I P位址的網絡号與我們的完全不同,則是非本地的;如果目的I P位址的網絡号與我們的相同而子網号與我們的不同,則可能是本地的,也可能是非本地的。大多數T C P實作版都提供了一個配置選項(附錄E和圖E - 1),讓系統管理者說明不同的子網是屬于本地還是非本地。這個選項的設定将确定M S S可以選擇盡可能的大(達到外出接口的M T U長度)或是預設值5 3 6。

M S S讓主機限制另一端發送資料報的長度。加上主機也能控制它發送資料報的長度,這将使以較小M T U連接配接到一個網絡上的主機避免分段。

隻有當一端的主機以小于5 7 6位元組的M T U直接連接配接到一個網絡中,避免這種分段才會有效。

如果兩端的主機都連接配接到以太網上,都采用5 3 6的M S S,但中間網絡采用2 9 6的M T U,也将會

出現分段。使用路徑上的M T U發現機制(參見2 4 . 2節)是關于這個問題的唯一方法。

       以上說明MSS的值可以通過協商解決,這個協商過程會涉及MTU的值的大小,前面說了:【MSS=外出接口上的MTU-IP首部-TCP首部】,我們來看看資料進入TCP協定棧的封裝過程:

關于TCP封包、粘包、半包關于Tcp封包

最後一層以太網幀的大小應該就是我們的出口MTU大小了。當目的主機收到一個以太網資料幀時,資料就開始從協定棧中由底向上升,同時去掉各層協定加上的封包首部。每層協定盒都要去檢查封包首部中的協定辨別,以确定接收資料的上層協定。這個過程稱作分用( D e m u l t i p l e x i n g),圖1 - 8顯示了該過程是如何發生的。

關于TCP封包、粘包、半包關于Tcp封包

那麼什麼是MTU呢,這實際上是資料鍊路層的一個概念,以太網和802.3這兩種區域網路技術标準都對“鍊路層”的資料幀有大小限制:

關于TCP封包、粘包、半包關于Tcp封包

l         最大傳輸單元MTU

正如在圖2 - 1看到的那樣,以太網和8 0 2 . 3對資料幀的長度都有一個限制,其最大值分别是1 5 0 0和1 4 9 2位元組。鍊路層的這個特性稱作M T U,最大傳輸單元。不同類型的網絡大多數都有一個上限。

如果I P層有一個資料報要傳,而且資料的長度比鍊路層的M T U還大,那麼I P層就需要進行分片( f r a g m e n t a t i o n),把資料報分成若幹片,這樣每一片都小于M T U。我們将在11 . 5節讨論I P分片的過程。

圖2 - 5列出了一些典型的M T U值,它們摘自RFC 1191[Mogul and Deering 1990]。點到點的鍊路層(如S L I P和P P P)的M T U并非指的是網絡媒體的實體特性。相反,它是一個邏輯限制,目的是為互動使用提供足夠快的響應時間。在2 . 1 0節中,我們将看到這個限制值是如何計算出來的。在3 . 9節中,我們将用n e t s t a t指令列印出網絡接口的M T U。

l         路徑MTU

當在同一個網絡上的兩台主機互相進行通信時,該網絡的M T U是非常重要的。但是如果

兩台主機之間的通信要通過多個網絡,那麼每個網絡的鍊路層就可能有不同的M T U。重要的

不是兩台主機所在網絡的M T U的值,重要的是兩台通信主機路徑中的最小M T U。它被稱作路

徑M T U。

兩台主機之間的路徑M T U不一定是個常數。它取決于當時所選擇的路由。而選路不一定

是對稱的(從A到B的路由可能與從B到A的路由不同),是以路徑M T U在兩個方向上不一定是

一緻的。

RFC 1191[Mogul and Deering 1990]描述了路徑M T U的發現機制,即在任何時候确定路徑

M T U的方法。我們在介紹了I C M P和I P分片方法以後再來看它是如何操作的。在11 . 6節中,我

們将看到I C M P的不可到達錯誤就采用這種發現方法。在11 . 7節中,還會看到, t r a c e r o u t e程式

也是用這個方法來确定到達目的節點的路徑M T U。在11 . 8節和2 4 . 2節,将介紹當産品支援路

徑M T U的發現方法時,U D P和T C P是如何進行操作的。

前面談到TCP如何保證傳輸可靠性是說到“當T C P發出一個段後,它啟動一個定時器,等待目的端确認收到這個封包段。如果不能及時收到一個确認,将重發這個封包段”,下面我看一下TCP的逾時與重傳。

T C P提供可靠的運輸層。它使用的方法之一就是确認從另一端收到的資料。但資料和确認都有可能會丢失。T C P通過在發送時設定一個定時器來解決這種問題。如果當定時器溢出時還沒有收到确認,它就重傳該資料。對任何實作而言,關鍵之處就在于逾時和重傳的政策,即怎樣決定逾時間隔和如何确定重傳的頻率。

對每個連接配接,T C P管理4個不同的定時器。

1) 重傳定時器使用于當希望收到另一端的确認。

2) 堅持( p e r s i s t )定時器使視窗大小資訊保持不斷流動,即使另一端關閉了其接收視窗。

3) 保活( k e e p a l i v e )定時器可檢測到一個空閑連接配接的另一端何時崩潰或重新開機。

4) 2MSL定時器測量一個連接配接處于T I M E _ WA I T狀态的時間。

T C P逾時與重傳中最重要的部分就是對一個給定連接配接的往返時間( RT T)的測量。由于路由器和網絡流量均會變化,是以我們認為這個時間可能經常會發生變化, T C P應該跟蹤這些變化并相應地改變其逾時時間。

大多數源于伯克利的T C P實作在任何時候對每個連接配接僅測量一次RT T值。在發送一個封包段時,如果給定連接配接的定時器已經被使用,則該封包段不被計時。

具體RTT值的估算比較麻煩,需要可以參考《TCP-IP詳解卷1第21章》

互動資料總是以小于最大封包段長度的分組發送。對于這些小的封包段,接收方使用經受時延的确認方法來判斷确認是否可被推遲發送,以便與回送資料一起發送。這樣通常會減少封包段的數目       。

通常T C P在接收到資料時并不立即發送A C K;相反,它推遲發送,以便将A C K與需要沿該方向發送的資料一起發送(有時稱這種現象為資料捎帶A C K)。絕大多數實作采用的時延為200 ms,也就是說,T C P将以最大200 ms 的時延等待是否有資料一起發送。

我們看看另一位朋友的blog對此的介紹:

摘要:當使用TCP傳輸小型資料包時,程式的設計是相當重要的。如果在設計方案中不對TCP資料包的 

延遲應答,Nagle算法,Winsock緩沖作用引起重視,将會嚴重影響程式的性能。這篇文章讨論了這些 

問題,列舉了兩個案例,給出了一些傳輸小資料包的優化設計方案。

背景:當Microsoft TCP棧接收到一個資料包時,會啟動一個200毫秒的計時器。當ACK确認資料包 

發出之後,計時器會複位,接收到下一個資料包時,會再次啟動200毫秒的計時器。為了提升應用程式 

在内部網和Internet上的傳輸性能,Microsoft TCP棧使用了下面的政策來決定在接收到資料包後 

什麼時候發送ACK确認資料包: 

1、如果在200毫秒的計時器逾時之前,接收到下一個資料包,則立即發送ACK确認資料包。 

2、如果目前恰好有資料包需要發給ACK确認資訊的接收端,則把ACK确認資訊附帶在資料包上立即發送。 

3、當計時器逾時,ACK确認資訊立即發送。 

為了避免小資料包擁塞網絡,Microsoft TCP棧預設啟用了Nagle算法,這個算法能夠将應用程式多次 

調用Send發送的資料拼接起來,當收到前一個資料包的ACK确認資訊時,一起發送出去。下面是Nagle 

算法的例外情況: 

1、如果Microsoft TCP棧拼接起來的資料包超過了MTU值,這個資料會立即發送,而不等待前一個資料 

包的ACK确認資訊。在以太網中,TCP的MTU(Maximum Transmission Unit)值是1460位元組。 

2、如果設定了TCP_NODELAY選項,就會禁用Nagle算法,應用程式調用Send發送的資料包會立即被 

投遞到網絡,而沒有延遲。 

為了在應用層優化性能,Winsock把應用程式調用Send發送的資料從應用程式的緩沖區複制到Winsock 

核心緩沖區。Microsoft TCP棧利用類似Nagle算法的方法,決定什麼時候才實際地把資料投遞到網絡。 

核心緩沖區的預設大小是8K,使用SO_SNDBUF選項,可以改變Winsock核心緩沖區的大小。如果有必要的話, 

Winsock能緩沖大于SO_SNDBUF緩沖區大小的資料。在絕大多數情況下,應用程式完成Send調用僅僅表明資料 

被複制到了Winsock核心緩沖區,并不能說明資料就實際地被投遞到了網絡上。唯一一種例外的情況是: 

通過設定SO_SNDBUT為0禁用了Winsock核心緩沖區。

Winsock使用下面的規則來向應用程式表明一個Send調用的完成: 

1、如果socket仍然在SO_SNDBUF限額内,Winsock複制應用程式要發送的資料到核心緩沖區,完成Send調用。 

2、如果Socket超過了SO_SNDBUF限額并且先前隻有一個被緩沖的發送資料在核心緩沖區,Winsock複制要發送 

的資料到核心緩沖區,完成Send調用。 

3、如果Socket超過了SO_SNDBUF限額并且核心緩沖區有不隻一個被緩沖的發送資料,Winsock複制要發送的資料 

到核心緩沖區,然後投遞資料到網絡,直到Socket降到SO_SNDBUF限額内或者隻剩餘一個要發送的資料,才 

完成Send調用。

案例1 

一個Winsock TCP用戶端需要發送10000個記錄到Winsock TCP服務端,儲存到資料庫。記錄大小從20位元組到100 

位元組不等。對于簡單的應用程式邏輯,可能的設計方案如下: 

1、用戶端以阻塞方式發送,服務端以阻塞方式接收。 

2、用戶端設定SO_SNDBUF為0,禁用Nagle算法,讓每個資料包單獨的發送。 

3、服務端在一個循環中調用Recv接收資料包。給Recv傳遞200位元組的緩沖區以便讓每個記錄在一次Recv調用中 

被擷取到。

性能: 

在測試中發現,用戶端每秒隻能發送5條資料到服務段,總共10000條記錄,976K位元組左右,用了半個多小時 

才全部傳到伺服器。

分析: 

因為用戶端沒有設定TCP_NODELAY選項,Nagle算法強制TCP棧在發送資料包之前等待前一個資料包的ACK确認 

資訊。然而,用戶端設定SO_SNDBUF為0,禁用了核心緩沖區。是以,10000個Send調用隻能一個資料包一個資料 

包的發送和确認,由于下列原因,每個ACK确認資訊被延遲200毫秒: 

1、當伺服器擷取到一個資料包,啟動一個200毫秒的計時器。 

2、服務端不需要向用戶端發送任何資料,是以,ACK确認資訊不能被發回的資料包順路攜帶。 

3、用戶端在沒有收到前一個資料包的确認資訊前,不能發送資料包。 

4、服務端的計時器逾時後,ACK确認資訊被發送到用戶端。

如何提高性能: 

在這個設計中存在兩個問題。第一,存在延時問題。用戶端需要能夠在200毫秒内發送兩個資料包到服務端。 

因為用戶端預設情況下使用Nagle算法,應該使用預設的核心緩沖區,不應該設定SO_SNDBUF為0。一旦TCP 

棧拼接起來的資料包超過MTU值,這個資料包會立即被發送,不用等待前一個ACK确認資訊。第二,這個設計 

方案對每一個如此小的的資料包都調用一次Send。發送這麼小的資料包是不很有效率的。在這種情況下,應該 

把每個記錄補充到100位元組并且每次調用Send發送80個記錄。為了讓服務端知道一次總共發送了多少個記錄, 

用戶端可以在記錄前面帶一個頭資訊。

案例二: 

一個Winsock TCP用戶端程式打開兩個連接配接和一個提供股票報價服務的Winsock TCP服務端通信。第一個連接配接 

作為指令通道用來傳輸股票編号到服務端。第二個連接配接作為資料通道用來接收股票報價。兩個連接配接被建立後, 

用戶端通過指令通道發送股票編号到服務端,然後在資料通道上等待傳回的股票報價資訊。用戶端在接收到第一 

個股票報價資訊後發送下一個股票編号請求到服務端。用戶端和服務端都沒有設定SO_SNDBUF和TCP_NODELAY 

選項。

測試中發現,用戶端每秒隻能擷取到5條報價資訊。

分析:

這個設計方案一次隻允許擷取一條股票資訊。第一個股票編号資訊通過指令通道發送到服務端,立即接收到 

服務端通過資料通道傳回的股票報價資訊。然後,用戶端立即發送第二條請求資訊,send調用立即傳回, 

發送的資料被複制到核心緩沖區。然而,TCP棧不能立即投遞這個資料包到網絡,因為沒有收到前一個資料包的 

ACK确認資訊。200毫秒後,服務端的計時器逾時,第一個請求資料包的ACK确認資訊被發送回用戶端,用戶端 

的第二個請求包才被投遞到網絡。第二個請求的報價資訊立即從資料通道傳回到用戶端,因為此時,用戶端的 

計時器已經逾時,第一個報價資訊的ACK确認資訊已經被發送到服務端。這個過程循環發生。

在這裡,兩個連接配接的設計是沒有必要的。如果使用一個連接配接來請求和接收報價資訊,股票請求的ACK确認資訊會 

被傳回的報價資訊立即順路攜帶回來。要進一步的提高性能,用戶端應該一次調用Send發送多個股票請求,服務端 

一次傳回多個報價資訊。如果由于某些特殊原因必須要使用兩個單向的連接配接,用戶端和服務端都應該設定TCP_NODELAY 

選項,讓小資料包立即發送而不用等待前一個資料包的ACK确認資訊。

提高性能的建議: 

上面兩個案例說明了一些最壞的情況。當設計一個方案解決大量的小資料包發送和接收時,應該遵循以下的建議: 

1、如果資料片段不需要緊急傳輸的話,應用程式應該将他們拼接成更大的資料塊,再調用Send。因為發送緩沖區 

很可能被複制到核心緩沖區,是以緩沖區不應該太大,通常比8K小一點點是很有效率的。隻要Winsock核心緩沖區 

得到一個大于MTU值的資料塊,就會發送若幹個資料包,剩下最後一個資料包。發送方除了最後一個資料包,都不會 

被200毫秒的計時器觸發。 

2、如果可能的話,避免單向的Socket資料流接連。 

3、不要設定SO_SNDBUF為0,除非想確定資料包在調用Send完成之後立即被投遞到網絡。事實上,8K的緩沖區适合大多數 

情況,不需要重新改變,除非新設定的緩沖區經過測試的确比預設大小更高效。 

4、如果資料傳輸不用保證可靠性,使用UDP。

1.         TCP提供了面向“連續位元組流”的可靠的傳輸服務,TCP并不了解流所攜帶的資料内容,這個内容需要應用層自己解析。

2.         “位元組流”是連續的、非結構化的,而我們的應用需要的是有序的、結構化的資料資訊,是以我們需要定義自己的“規則”去解讀這個“連續的位元組流“,那解決途徑就是定義自己的封包類型,然後用這個類型去映射“連續位元組流”。

如何定義封包,我們回顧一下前面這個資料進入協定棧的封裝過程圖:

關于TCP封包、粘包、半包關于Tcp封包

封包其實就是将上圖中進入協定棧的使用者資料[即使用者要發送的資料]定義為一種友善識别和交流的類型,這有點類似信封的概念,信封就是一種人們之間通信的格式,信封格式如下:

信封格式:

       收信人郵編

       收信人位址

       收信人姓名

       信件内容

那麼在程式裡面我們也需要定義這種格式:在C++裡面隻有結構和類這種兩種類型适合表達這個概念了。網絡上很多朋友對此表述了自己的看法并貼出了代碼:比如

       /************************************************************************/

/* 資料封包資訊定義開始                                                 */

/************************************************************************/

#pragma pack(push,1)   //将原對齊方式壓棧,采用新的1位元組對齊方式

/* 封包類型枚舉[此處根據需求列舉] */

typedef enum{

              NLOGIN=1,

              NREG=2,

              NBACKUP=3,

              NRESTORE=3,

              NFILE_TRANSFER=4,

              NHELLO=5

} PACKETTYPE;

/* 標頭 */

typedef struct tagNetPacketHead{

       byte version;//版本

       PACKETTYPE ePType;//包類型

       WORD nLen;//包體長度

} NetPacketHead;

/* 封包對象[標頭&amp;包體] */

typedef struct tagNetPacket{

       NetPacketHead netPacketHead;//標頭

       char * packetBody;//包體

} NetPacket;

#pragma pack(pop)

/**************資料封包資訊定義結束**************************/

3.         發包順序與收包問題

a)         由于TCP要通過協商解決發送出去的封包段的長度,是以我們發送的資料很有可能被分割甚至被分割後再重組交給網絡層發送,而網絡層又是采用分組傳送,即網絡層資料報到達目标的順序完全無法預測,那麼收包會出現半包、粘包問題。舉個例子,發送端連續發送兩端資料msg1和msg2,那麼發送端[傳輸層]可能會出現以下情況:

                                       i.              Msg1和msg2小于TCP的MSS,兩個包按照先後順序被發出,沒有被分割和重組

                                     ii.              Msg1過大被分割成兩段TCP封包msg1-1、msg2-2進行傳送,msg2較小直接被封裝成一個封包傳送

                                    iii.              Msg1過大被分割成兩段TCP封包msg1-1、msg2-2,msg1-1先被傳送,剩下的msg1-2和msg2[較小]被組合成一個封包傳送

                                   iv.              Msg1過大被分割成兩段TCP封包msg1-1、msg2-2,msg1-1先被傳送,剩下的msg1-2和msg2[較小]組合起來還是太小,組合的内容在和後面再發送的msg3的前部分資料組合起來發送

                                     v.              ……………………….太多……………………..

b)        接收端[傳輸層]可能出現的情況

                                       i.              先收到msg1,再收到msg2,這種方式太順利了。

                                     ii.              先收到msg1-1,再收到msg1-2,再收到msg2

                                    iii.              先收到msg1,再收到msg2-1,再收到msg2-2

                                   iv.              先收到msg1和msg2-1,再收到msg2-2

                                     v.              //…………還有很多………………

c)        其實“接收端網絡層”接收到的分組資料報順序和發送端比較可能完全是亂的,比如發“送端網絡層”發送1、2、3、4、5,而接收端網絡層接收到的資料報順序卻可能是2、1、5、4、3,但是“接收端的傳輸層”會保證連結的有序性和可靠性,“接收端的傳輸層”會對“接收端網絡層”收到的順序紊亂的資料報重組成有序的封包[即發送方傳輸層發出的順序],然後交給“接收端應用層”使用,是以“接收端傳輸層”總是能夠保證資料包的有序性,“接收端應用層”[我們編寫的socket程式]不用擔心接收到的資料的順序問題。

d)        但是如上所述,粘包問題和半包問題不可避免。我們在接收端應用層需要自己編碼處理粘包和半包問題。一般做法是定義一個緩沖區或者是使用标準庫/架構提供的容器循環存放接收到資料,邊接收變判斷緩沖區資料是否滿足標頭大小,如果滿足標頭大小再判斷緩沖區剩下資料是否滿足包體大小,如果滿足則提取。詳細步驟如下:

1.         接收資料存入緩沖區尾部

2.         緩沖區資料滿足標頭大小否

3.         緩沖區資料不滿足標頭大小,回到第1步;緩沖區資料滿足標頭大小則取出標頭,接着判斷緩沖區剩餘資料滿足標頭中定義的包體大小否,不滿足則回到第1步。

4.         緩沖區資料滿足一個標頭大小和一個包體大小之和,則取出標頭和包體進行使用,此處使用可以采用拷貝方式轉移緩沖區資料到另外一個地方,也可以為了節省記憶體直接采取調用回調函數的方式完成資料使用。

5.         清除緩沖區的第一個標頭和包體資訊,做法一般是将緩沖區剩下的資料拷貝到緩沖區首部覆寫“第一個標頭和包體資訊”部分即可。

粘包、半包處理具體實作很多朋友都有自己的做法,比如最前面貼出的連結,這裡我也貼出一段參考:

緩沖區實作頭檔案:

#include &lt;windows.h&gt;

#ifndef _CNetDataBuffer_H_

#define _CNetDataBuffer_H_

#ifndef TCPLAB_DECLSPEC

#define TCPLAB_DECLSPEC _declspec(dllimport)

#endif

//緩沖區初始大小

#define BUFFER_INIT_SIZE 2048

//緩沖區膨脹系數[緩沖區膨脹後的大小=原大小+系數*新增資料長度]

#define BUFFER_EXPAND_SIZE 2

//計算緩沖區除第一個標頭外剩下的資料的長度的宏[緩沖區資料總長度-標頭大小]

#define BUFFER_BODY_LEN (m_nOffset-sizeof(NetPacketHead))

//計算緩沖區資料目前是否滿足一個完整包資料量[標頭&amp;包體]

#define HAS_FULL_PACKET ( \

                                                 (sizeof(NetPacketHead)&lt;=m_nOffset) &amp;&amp; \

                                                 ((((NetPacketHead*)m_pMsgBuffer)-&gt;nLen) &lt;= BUFFER_BODY_LEN) \

                                          )

//檢查包是否合法[包體長度大于零且包體不等于空]

#define IS_VALID_PACKET(netPacket) \

       ((netPacket.netPacketHead.nLen&gt;0) &amp;&amp; (netPacket.packetBody!=NULL))

//緩沖區第一個包的長度

#define FIRST_PACKET_LEN (sizeof(NetPacketHead)+((NetPacketHead*)m_pMsgBuffer)-&gt;nLen)

/* 資料緩沖 */

class /*TCPLAB_DECLSPEC*/ CNetDataBuffer

{

       /* 緩沖區操作相關成員 */

private:

       char *m_pMsgBuffer;//資料緩沖區

       int m_nBufferSize;//緩沖區總大小

       int m_nOffset;//緩沖區資料大小

public:

       int GetBufferSize() const;//獲得緩沖區的大小

       BOOL ReBufferSize(int);//調整緩沖區的大小

       BOOL IsFitPacketHeadSize() const;//緩沖資料是否适合標頭大小

       BOOL IsHasFullPacket() const;//緩沖區是否擁有完整的包資料[包含標頭和包體]      

       BOOL AddMsg(char *pBuf,int nLen);//添加消息到緩沖區

       const char *GetBufferContents() const;//得到緩沖區内容

       void Reset();//緩沖區複位[清空緩沖區資料,但并未釋放緩沖區]

       void Poll();//移除緩沖區首部的第一個資料包

       CNetDataBuffer();

       ~CNetDataBuffer();

};

緩沖區實作檔案:

#define TCPLAB_DECLSPEC _declspec(dllexport)

#include "CNetDataBuffer.h"

/* 構造 */

CNetDataBuffer::CNetDataBuffer()

       m_nBufferSize = BUFFER_INIT_SIZE;//設定緩沖區大小

       m_nOffset = 0;//設定資料偏移值[資料大小]為0

       m_pMsgBuffer = NULL;

       m_pMsgBuffer = new char[BUFFER_INIT_SIZE];//配置設定緩沖區為初始大小

       ZeroMemory(m_pMsgBuffer,BUFFER_INIT_SIZE);//緩沖區清空   

}

/* 析構 */

CNetDataBuffer::~CNetDataBuffer()

       if (m_nOffset!=0)

       {

              delete [] m_pMsgBuffer;//釋放緩沖區

              m_pMsgBuffer = NULL;

              m_nBufferSize=0;

              m_nOffset=0;

       }

/* Description:       獲得緩沖區中資料的大小                                  */

/* Return:              緩沖區中資料的大小                                                                   */

INT CNetDataBuffer::GetBufferSize() const

       return this-&gt;m_nOffset;

/* Description:       緩沖區中的資料大小是否足夠一個標頭大小                  */

/* Return:              如果滿足則傳回True,否則傳回False

BOOL CNetDataBuffer::IsFitPacketHeadSize() const

       return sizeof(NetPacketHead)&lt;=m_nOffset;

/* Description:       判斷緩沖區是否擁有完整的資料包(標頭和包體)              */

/* Return:              如果緩沖區包含一個完整封包則傳回True,否則False                   */

BOOL CNetDataBuffer::IsHasFullPacket() const

       //如果連標頭大小都不滿足則傳回

       //if (!IsFitPacketHeadSize())

       //     return FALSE;

       return HAS_FULL_PACKET;//此處采用宏簡化代碼

/* Description:       重置緩沖區大小                                                  */

/* nLen:          新增加的資料長度                                                                      */

/* Return:              調整結果                                                                                    */

BOOL CNetDataBuffer::ReBufferSize(int nLen)

       char *oBuffer = m_pMsgBuffer;//儲存原緩沖區位址

       try

              nLen=(nLen&lt;64?64:nLen);//保證最小增量大小

              //新緩沖區的大小=增加的大小+原緩沖區大小

              m_nBufferSize = BUFFER_EXPAND_SIZE*nLen+m_nBufferSize;          

              m_pMsgBuffer = new char[m_nBufferSize];//配置設定新的緩沖區,m_pMsgBuff指向新緩沖區位址

              ZeroMemory(m_pMsgBuffer,m_nBufferSize);//新緩沖區清零

              CopyMemory(m_pMsgBuffer,oBuffer,m_nOffset);//将原緩沖區的内容全部拷貝到新緩沖區

       catch(...)

              throw;

       delete []oBuffer;//釋放原緩沖區

       return TRUE;

/* Description:       向緩沖區添加消息                                        */

/* pBuf:          要添加的資料                                                                             */

/* nLen:          添加的消息長度

/* return:        添加成功傳回True,否則False                                                      */

BOOL CNetDataBuffer::AddMsg(char *pBuf,int nLen)

              //檢查緩沖區長度是否滿足,不滿足則重新調整緩沖區大小

              if (m_nOffset+nLen&gt;m_nBufferSize)

                     ReBufferSize(nLen);

              //拷貝新資料到緩沖區末尾 

              CopyMemory(m_pMsgBuffer+sizeof(char)*m_nOffset,pBuf,nLen);

              m_nOffset+=nLen;//修改資料偏移

              return FALSE;

/* 得到緩沖區内容 */

const char * CNetDataBuffer::GetBufferContents() const

       return m_pMsgBuffer;

/* 緩沖區複位                                                           */

void CNetDataBuffer::Reset()

       if (m_nOffset&gt;0)

              m_nOffset = 0;

              ZeroMemory(m_pMsgBuffer,m_nBufferSize);

/* 移除緩沖區首部的第一個資料包                                         */

void CNetDataBuffer::Poll()

       if(m_nOffset==0 || m_pMsgBuffer==NULL)

              return;

       if (IsFitPacketHeadSize() &amp;&amp; HAS_FULL_PACKET)

       {           

              CopyMemory(m_pMsgBuffer,m_pMsgBuffer+FIRST_PACKET_LEN*sizeof(char),m_nOffset-FIRST_PACKET_LEN);

對TCP發包和收包進行簡單封裝:

頭檔案:

// #ifndef TCPLAB_DECLSPEC

// #define TCPLAB_DECLSPEC _declspec(dllimport)

// #endif

#ifndef _CNETCOMTEMPLATE_H_

#define _CNETCOMTEMPLATE_H_

//通信端口

#define TCP_PORT 6000

/* 通信終端[包含一個Socket和一個緩沖對象] */

typedef struct {

       SOCKET m_socket;//通信套接字

       CNetDataBuffer m_netDataBuffer;//該套接字關聯的資料緩沖區

} ComEndPoint;

/* 收包回調函數參數 */

typedef struct{

       NetPacket *pPacket;

       LPVOID processor;

       SOCKET comSocket;

} PacketHandlerParam;

class CNetComTemplate{     

       /* Socket操作相關成員 */

       void SendPacket(SOCKET m_connectedSocket,NetPacket &amp;netPacket);//發包函數

       BOOL RecvPacket(ComEndPoint &amp;comEndPoint,void (*recvPacketHandler)(LPVOID)=NULL,LPVOID=NULL);//收包函數

       CNetComTemplate();

       ~CNetComTemplate();

實作檔案:

#include "CNetComTemplate.h"

CNetComTemplate::CNetComTemplate()

CNetComTemplate::~CNetComTemplate()

/* Description:發包                                                     */

/* m_connectedSocket:建立好連接配接的套接字                                                             */

/* netPacket:要發送的資料包                                                                                   */

void CNetComTemplate::SendPacket(SOCKET m_connectedSocket,NetPacket &amp;netPacket)

       if (m_connectedSocket==NULL || !IS_VALID_PACKET(netPacket))//如果尚未建立連接配接則退出

       ::send(m_connectedSocket,(char*)&amp;netPacket.netPacketHead,sizeof(NetPacketHead),0);//先發送標頭

       ::send(m_connectedSocket,netPacket.packetBody,netPacket.netPacketHead.nLen,0);//在發送包體

/**************************************************************************/

/* Description:收包                                                       */

/* comEndPoint:通信終端[包含套接字和關聯的緩沖區]                                     */

/* recvPacketHandler:收包回調函數,當收到一個包後調用該函數進行包的分發處理*/

BOOL CNetComTemplate::RecvPacket(ComEndPoint &amp;comEndPoint,void (*recvPacketHandler)(LPVOID),LPVOID pCallParam)

       if (comEndPoint.m_socket==NULL)

       int nRecvedLen = 0;

       char pBuf[1024];

       //如果緩沖區資料不夠包大小則繼續從套接字讀取tcp封包段

       while (!(comEndPoint.m_netDataBuffer.IsHasFullPacket()))

              nRecvedLen = recv(comEndPoint.m_socket,pBuf,1024,0);

              if (nRecvedLen==SOCKET_ERROR || nRecvedLen==0)//若果Socket錯誤或者對方連接配接已經正常關閉則結束讀取

                     break;

              comEndPoint.m_netDataBuffer.AddMsg(pBuf,nRecvedLen);//将新接收的資料存入緩沖區

       //執行到此處可能是三種情況:

       //1.已經讀取到的資料滿足一個完整的tcp封包段

       //2.讀取發生socket_error錯誤

       //3.在還未正常讀取完畢的過程中對方連接配接已經關閉

       //如果沒有讀取到資料或者沒有讀取到完整封包段則傳回傳回

       if (nRecvedLen==0 || (!(comEndPoint.m_netDataBuffer.IsHasFullPacket())))

       if (recvPacketHandler!=NULL)

              //構造準備傳遞給回調函數的資料包

              NetPacket netPacket;

              netPacket.netPacketHead = *(NetPacketHead*)comEndPoint.m_netDataBuffer.GetBufferContents();

              netPacket.packetBody = new char[netPacket.netPacketHead.nLen];//動态配置設定包體空間

              //構造回調函數參數

              PacketHandlerParam packetParam;

              packetParam.pPacket = &amp;netPacket;

              packetParam.processor = pCallParam;

              //呼叫回調函數

              recvPacketHandler(&amp;packetParam);

              delete []netPacket.packetBody;

       //移除緩沖區的第一個包

       comEndPoint.m_netDataBuffer.Poll();

繼續閱讀