網絡之 TCP封包、粘包、半包

TCP大致工作原理介紹:

工作原理

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

儘管T C PU 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的內核來說,它無法區分一個二進制文件與一個文本文件。

T C P如何確定報文段的長度

       我仍然引用官方解釋《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 PT 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 6M S S,但中間網絡採用2 9 6M T U,也將會

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

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

       clip_image002

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

clip_image004

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

clip_image006

l         最大傳輸單元MTU

 

正如在圖2 - 1看到的那樣,以太網和8 0 2 . 3對數據幀的長度都有一個限制,其最大值分別是1 5 0 01 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 PP 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不一定是個常數。它取決於當時所選擇的路由。而選路不一定

是對稱的(從AB的路由可能與從BA的路由不同),因此路徑M T U在兩個方向上不一定是

一致的。

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

M T U的方法。我們在介紹了I C M PI 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 PT C P是如何進行操作的。

TCP的超時與重傳

前面談到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詳解卷121章》

TCP經受延時的確認

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

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

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

http://blog.csdn.net/lqhed/article/details/52563739

結論:

1.         TCP提供了面向“連續字節流”的可靠的傳輸服務,TCP並不理解流所攜帶的數據內容,這個內容需要應用層自己解析。

2.         “字節流”是連續的、非結構化的,而我們的應用需要的是有序的、結構化的數據信息,因此我們需要定義自己的“規則”去解讀這個“連續的字節流“,那解決途徑就是定義自己的封包類型,然後用這個類型去映射“連續字節流”。

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

clip_image007

封包其實就是將上圖中進入協議棧的用戶數據[即用戶要發送的數據]定義爲一種方便識別和交流的類型,這有點類似信封的概念,信封就是一種人們之間通信的格式,信封格式如下:

信封格式:

       收信人郵編

       收信人地址

       收信人姓名

       信件內容

那麼在程序裏面我們也需要定義這種格式:在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;

 

/* 封包對象[包頭&包體] */

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 <windows.h>

 

#ifndef _CNetDataBuffer_H_

#define _CNetDataBuffer_H_

 

#ifndef TCPLAB_DECLSPEC

#define TCPLAB_DECLSPEC _declspec(dllimport)

#endif

 

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

/* 數據封包信息定義開始                                                 */

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

 

#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;

 

/* 封包對象[包頭&包體] */

typedef struct tagNetPacket{

       NetPacketHead netPacketHead;//包頭

       char * packetBody;//包體

} NetPacket;

 

#pragma pack(pop)

/**************數據封包信息定義結束**************************/

 

//緩衝區初始大小

#define BUFFER_INIT_SIZE 2048

 

//緩衝區膨脹係數[緩衝區膨脹後的大小=原大小+係數*新增數據長度]

#define BUFFER_EXPAND_SIZE 2

 

//計算緩衝區除第一個包頭外剩下的數據的長度的宏[緩衝區數據總長度-包頭大小]

#define BUFFER_BODY_LEN (m_nOffset-sizeof(NetPacketHead))

 

//計算緩衝區數據當前是否滿足一個完整包數據量[包頭&包體]

#define HAS_FULL_PACKET ( \

                                                 (sizeof(NetPacketHead)<=m_nOffset) && \

                                                 ((((NetPacketHead*)m_pMsgBuffer)->nLen) <= BUFFER_BODY_LEN) \

                                          )

 

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

#define IS_VALID_PACKET(netPacket) \

       ((netPacket.netPacketHead.nLen>0) && (netPacket.packetBody!=NULL))

 

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

#define FIRST_PACKET_LEN (sizeof(NetPacketHead)+((NetPacketHead*)m_pMsgBuffer)->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();//移除緩衝區首部的第一個數據包

public:

       CNetDataBuffer();

       ~CNetDataBuffer();

};

 

#endif

 

緩衝區實現文件:

#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->m_nOffset;

}

 

 

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

/* Description:       緩衝區中的數據大小是否足夠一個包頭大小                  */

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

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

BOOL CNetDataBuffer::IsFitPacketHeadSize() const

{

       return sizeof(NetPacketHead)<=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<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)

{

       try

       {

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

              if (m_nOffset+nLen>m_nBufferSize)

                     ReBufferSize(nLen);

             

              //拷貝新數據到緩衝區末尾 

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

             

              m_nOffset+=nLen;//修改數據偏移

       }

       catch(...)

       {

              return FALSE;

       }

       return TRUE;

}

 

/* 得到緩衝區內容 */

const char * CNetDataBuffer::GetBufferContents() const

{

       return m_pMsgBuffer;

}

 

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

/* 緩衝區復位                                                           */

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

void CNetDataBuffer::Reset()

{

      

       if (m_nOffset>0)

       {

              m_nOffset = 0;

              ZeroMemory(m_pMsgBuffer,m_nBufferSize);

       }

}

 

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

/* 移除緩衝區首部的第一個數據包                                         */

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

void CNetDataBuffer::Poll()

{

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

              return;

       if (IsFitPacketHeadSize() && HAS_FULL_PACKET)

       {           

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

       }

}

 

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

頭文件:

#include <windows.h>

#include "CNetDataBuffer.h"

 

// #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操作相關成員 */

private:

      

public:

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

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

public:

       CNetComTemplate();

       ~CNetComTemplate();

};

 

#endif

 

 

實現文件:

 

#include "CNetComTemplate.h"

 

CNetComTemplate::CNetComTemplate()

{

 

}

 

CNetComTemplate::~CNetComTemplate()

{

 

}

 

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

/* Description:發包                                                     */

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

/* netPacket:要發送的數據包                                                                                   */

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

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

{

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

       {

              return;

       }

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

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

}

 

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

/* Description:收包                                                       */

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

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

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

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

{

       if (comEndPoint.m_socket==NULL)

              return FALSE;

      

       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())))

       {

              return FALSE;

       }

 

       if (recvPacketHandler!=NULL)

       {

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

              NetPacket netPacket;

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

              netPacket.packetBody = new char[netPacket.netPacketHead.nLen];//動態分配包體空間

             

              //構造回調函數參數

              PacketHandlerParam packetParam;

              packetParam.pPacket = &netPacket;

              packetParam.processor = pCallParam;

 

              //呼叫回調函數

              recvPacketHandler(&packetParam);

 

              delete []netPacket.packetBody;

       }

 

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

       comEndPoint.m_netDataBuffer.Poll();

 

       return TRUE;

}


感謝http://www.cnblogs.com/jiangtong/archive/2012/03/22/2411985.html



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