問題產生
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);
以上就是解決簡單的解決數據包粘包和半包的方法,結尾應該加上一個結尾符。