Socket學習之解決TCP半包粘包問題

問題產生

TCP是一個數據流協議,所以TCP發送的數據包大小是不可控制的,這時候就會出現粘包和半包的現象,下面這張圖是我從網上找的,描述很形象


1. 情況1,Data1和Data2都分開發送到了Server端,沒有產生粘包和拆包的情況。

2. 情況2,Data1和Data2數據粘在了一起,打成了一個大的包發送到Server端,這個情況就是粘包

3. 情況3,Data2被分離成Data2_1和Data2_2,並且Data2_1在Data1之前到達了服務端,這種情況就產生了拆包

由於網絡的複雜性,可能數據會被分離成N多個複雜的拆包/粘包的情況,所以在做TCP服務器的時候就需要首先解決拆包/粘包的問題。


TCP粘包和拆包產生的原因

1. 應用程序寫入數據的字節大小大於套接字發送緩衝區的大小

2. 進行MSS大小的TCP分段。MSS是最大報文段長度的縮寫。MSS是TCP報文段中的數據字段的最大長度。數據字段加上TCP首部纔等於整個的TCP報文段。所以MSS並不是TCP報文段的最大長度,而是:MSS=TCP報文段長度-TCP首部長度

3. 以太網的payload大於MTU進行IP分片。MTU指:一種通信協議的某一層上面所能通過的最大數據包大小。如果IP層有一個數據包要傳,而且數據的長度比鏈路層的MTU大,那麼IP層就會進行分片,把數據包分成託乾片,讓每一片都不超過MTU。注意,IP分片可以發生在原始發送端主機上,也可以發生在中間路由器上。


TCP粘包和拆包的解決策略

1. 消息定長。例如100字節。

2. 在包尾部增加回車或者空格符等特殊字符進行分割,典型的如FTP協議

3. 將消息分爲消息頭和消息尾。

   我使用方法3解決數據包粘包和半包的情況


  數據包分爲

  包頭+包體

  包頭


/// 網絡數據包包頭  
struct NetPacketHeader  
{  
unsigned short      wDataSize;  ///< 數據包大小,包含封包頭和封包數據大小  
unsigned short      wOpcode;    ///< 操作碼  
};

包體

struct NetPacket  
{  
NetPacketHeader     Header;                         ///< 包頭  
unsigned char       Data[NET_PACKET_DATA_SIZE];     ///< 數據  
};    



/// 網絡操作碼  
enum eNetOpcode  
{  
NET_TEST1           = 1,  
};  


/// 測試1的網絡數據包定義  
struct NetPacket_Test1  
{  
int     nIndex;  
char name[20];  
char sex[20];  
int age;  
char    arrMessage[512];  
};  


封包方法


bool TCPServer::SendData( unsigned short nOpcode, const char* pDataBuffer, const unsigned int& nDataSize )  
{  
NetPacketHeader* pHead = (NetPacketHeader*) m_cbSendBuf;  
pHead->wOpcode = nOpcode;//操作碼  


// 數據封包  
if ( (nDataSize > 0) && (pDataBuffer != 0) )  
{  
CopyMemory(pHead+1, pDataBuffer, nDataSize);   
}  


// 發送消息  
const unsigned short nSendSize = nDataSize + sizeof(NetPacketHeader);//包的大小事發送數據的大小加上包頭大小  
pHead->wDataSize = nSendSize;//包大小  
int ret = ::send(mAcceptSocket, m_cbSendBuf, nSendSize, 0); 
return (ret > 0) ? true : false;  
}  


拆包策略

//TCP會存在有時候發送半包的情況,所以事先要檢測接受的數據是否大於包頭的長度,如果大於的話就接受並解析包頭,包頭的大小是固定的


int nRecvSize = ::recv(mServerSocket,  
m_cbRecvBuf+m_nRecvSize,   
sizeof(m_cbRecvBuf)-m_nRecvSize, 0);  



// 保存已經接收數據的大小  
m_nRecvSize += nRecvSize;  


// 接收到的數據夠不夠一個包頭的長度  
while (m_nRecvSize >= sizeof(NetPacketHeader))//已經收到一個完整的包,如果沒用收到一個完整的包,此處循環不執行,繼續下一輪循環  



// 讀取包頭  
NetPacketHeader* pHead = (NetPacketHeader*) (m_cbRecvBuf);  
const unsigned short nPacketSize = pHead->wDataSize;  


// 判斷是否已接收到足夠一個完整包的數據  
if (m_nRecvSize < nPacketSize)  
{  
// 還不夠拼湊出一個完整包  
break;  
}  


// 拷貝到數據緩存  
CopyMemory(m_cbDataBuf, m_cbRecvBuf, nPacketSize);  


// 從接收緩存移除  
MoveMemory(m_cbRecvBuf, m_cbRecvBuf+nPacketSize, m_nRecvSize);  
m_nRecvSize -= nPacketSize;  


// 解密數據,以下省略一萬字  
// ...  


// 分派數據包,讓應用層進行邏輯處理  
pHead = (NetPacketHeader*) (m_cbDataBuf);  
const unsigned short nDataSize = nPacketSize - (unsigned short)sizeof(NetPacketHeader);  
OnNetMessage(pHead->wOpcode, m_cbDataBuf+sizeof(NetPacketHeader), nDataSize);  


以上就是解決簡單的解決數據包粘包和半包的方法,結尾應該加上一個結尾符。



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