TCP编程之二 粘包分包处理逻辑

基于TCP的网络编程中, 数据传输是基于连接的,所以当网络出现堵塞或者发送频率过高的时候,就会出现粘包的情况。

粘包就是并不是一个接收对应一个发送,有可能一个接收对应多个发送,也可能一个接收少于一个发送。

由于我们在网络编程中,经常以对象作为发送的单元,所以接受端必须对粘包做处理,还原原来的对象。


下图说明了接受端接收到数据的各种情况:

TCP编程之二 粘包分包处理逻辑

当然,接收到第一种情况是最理想的,也不须处理。本文针对2 3 4情况做处理。

算法解析:

   首先有一个对象用于保存上次未能处理的数据,和上次为处理数据的长度。

1.  将本次接收到的数据拼接到上一次未处理数据后面,为未处理数据。

2.  判断未处理数据长度是否大于包头,

     若小于包头,直接退出(包头保存长度信息) , 否则转3。

3. 根据包头判断对象大小是否大于未处理数据长度,若是转3, 否则保存未处理数据退出。

4. 截出第一个对象进行处理,剩下的数据重新保存到未处理对象,继续转2循环.

[cpp] view plain copy
  1. // TcpDataSplit.cpp : 定义控制台应用程序的入口点。  
  2. //  
  3.   
  4. #include "stdafx.h"  
  5. #include <stdio.h>  
  6. #include <stdlib.h>  
  7. #include <string.h>  
  8.   
  9. #define MAX_NETPACK_SIZE    10000  
  10. #define MAX_DATA_SIZE           4086  
  11.   
  12. /* 数据包头类型 */  
  13. struct NetDataHeader_t  
  14. {  
  15.     int nDataType;                                          //数据包类型,标识对应的对象类型  
  16.     int nDataSize;                                          //数据包中szData真实数据的长度  
  17. };  
  18.   
  19. /*  数据包类型 */  
  20. struct NetDataBase_t  
  21. {  
  22.     NetDataHeader_t  dataHeader;            //数据包头  
  23.     char     szData[MAX_DATA_SIZE];             //真实数据  
  24. };  
  25.   
  26. /** 
  27.     其实NetDataBase_t是基础类型,由此我们可以延伸出很多子类型, 
  28.     所以我们要清楚,每个类型的长度是不一样的,不都是sizeof(NetDataBase_t), 
  29.     就是各个类型对象大小不一样,比如: 
  30.     在派生结构体中,NetDataPeople_t和NetDataSchool_t是两个各异的结构体, 
  31.     但他们都有相关的Header部分指明结构体类型和长度。 
  32. */  
  33. struct NetDataPeople_t  
  34. {  
  35.     NetDataHeader_t dataHeader;   
  36.     int     nAge;  
  37.     char    szName[10];  
  38. };  
  39.   
  40. struct NetDataSchool_t  
  41. {  
  42.     NetDataHeader_t dataHeader;   
  43.     char    szShoolName[20];  
  44.     char    szShoolAddress[30];  
  45. };  
  46.   
  47. /** 
  48.     处理整理好的对象。 
  49. */  
  50. bool HandleNetPack(NetDataHeader_t* pDataHeader);  
  51.   
  52.   
  53. bool TcpDataSplit(const char* szRecNetData, int nRecSize)  
  54. {  
  55.     /** 
  56.         对于szLastSaveData, nRemainSize,为了简单,本例子只 
  57.         作为静态变量使用,因此只限于一个socket的数据接收, 
  58.         假如要同时处理多个socket数据,请放在对应容器里保存 
  59.     */  
  60.     static char szLastSaveData[MAX_NETPACK_SIZE];  
  61.     static int nRemainSize = 0;  
  62.     static bool bFirst = true;  
  63.   
  64.     if (bFirst)  
  65.     {  
  66.         memset(szLastSaveData, 0, sizeof(szLastSaveData));  
  67.         bFirst = false;  
  68.     }  
  69.   
  70.     /* 本次接收到的数据拼接到上次数据 */  
  71.     memcpy( (char*)(szLastSaveData+nRemainSize), szRecNetData, nRecSize );  
  72.     nRemainSize = nRecSize + nRemainSize;  
  73.   
  74.     /* 强制转换成NetDataPack指针 */  
  75.     NetDataHeader_t* pDataHead = (NetDataHeader_t*)szLastSaveData;  
  76.   
  77.     /** 
  78.        核心算法  
  79.     */  
  80.     while ( nRemainSize >sizeof(NetDataHeader_t) &&  
  81.                 nRemainSize >= pDataHead->nDataSize +sizeof(NetDataHeader_t) )  
  82.     {  
  83.             HandleNetPack(pDataHead);  
  84.             int  nRecObjectSize = sizeof(NetDataHeader_t) + pDataHead->nDataSize;        //本次收到对象的大小  
  85.             nRemainSize -= nRecObjectSize ;               
  86.             pDataHead = (NetDataHeader_t*)( (char*)pDataHead + nRecObjectSize );        //移动下一个对象头  
  87.     }  
  88.       
  89.     /* 余下数据未能组成一个对象,先保存起来 */  
  90.     if (szLastSaveData != (char*)pDataHead)  
  91.     {  
  92.         memmove(szLastSaveData, (char*)pDataHead, nRemainSize);  
  93.         memset( (char*)( szLastSaveData+nRemainSize), 0, sizeof(szLastSaveData)-nRemainSize );  
  94.     }  
  95.       
  96.     return true;  
  97. }  
  98.   
  99.   
  100. /** 
  101.     处理整理好的对象。 
  102. */  
  103. bool HandleNetPack(NetDataHeader_t* pDataHeader)  
  104. {  
  105.     //处理数据包  
  106.     if  (pDataHeader->nDataType == 1)  
  107.     {  
  108.         NetDataPeople_t* pPeople = (NetDataPeople_t*)pDataHeader;  
  109.         printf("收到People对象,Age:%d, Name:%s\n", pPeople->nAge, pPeople->szName);  
  110.     }  
  111.     else if (pDataHeader->nDataType == 2)  
  112.     {  
  113.         NetDataSchool_t* pSchool = (NetDataSchool_t*)pDataHeader;  
  114.         printf("收到School对象,SchoolName:%s, SchoolAddress:%s\n", pSchool->szShoolName, pSchool->szShoolAddress);  
  115.     }  
  116.   
  117.     return true;  
  118. }  
  119.   
  120. int _tmain(int argc, _TCHAR* argv[])  
  121. {  
  122.     /* 本例子以两个对象作为接收到的数据 */  
  123.     NetDataPeople_t  people;  
  124.     people.dataHeader.nDataSize = sizeof(people) - sizeof(NetDataHeader_t);  
  125.     people.dataHeader.nDataType = 1;  
  126.     people.nAge = 20;  
  127.     sprintf(people.szName, "Jim");      //real data  
  128.   
  129.     NetDataSchool_t  school;  
  130.     school.dataHeader.nDataSize = sizeof(school) - sizeof(NetDataHeader_t);  
  131.     school.dataHeader.nDataType = 2;  
  132.     sprintf(school.szShoolName, "清华大学");        //real data  
  133.     sprintf(school.szShoolAddress, "北京市北京路");       //real data  
  134.   
  135.     /* 将两个对象数据合并到一个地址里面以便重现粘包 */  
  136.     char szSendData[sizeof(people)+sizeof(school)];  
  137.     memcpy(szSendData,  (char*)&people,  sizeof(people));  
  138.     memcpy(szSendData+sizeof(people),  (char*)&school,  sizeof(school));  
  139.   
  140.     //这里进行收数据操作,这里省略。。。  
  141.   
  142.     /** 
  143.         特意设置粘包: 
  144.         1.第一次只发送3个字节,还不足以构建包头 
  145.         2.第二次发送10个字节,总共13个,但第一个对象大小是8+14=18;因此第一个对象people还没收满 
  146.         3.第三次发送剩下的全部,第一个对象剩下的部分与第二个对象粘在一起,验证处理 
  147.     */  
  148.     TcpDataSplit((char*)szSendData, 3); 
  149.  //在这里传递值3为recv的返回值。比如int i = recv(); TcpDataSplit((char*)szSendData, i);
  150.     TcpDataSplit((char*)szSendData, recv(....))
  151.     TcpDataSplit((char*)szSendData+3,  10);  
  152.     TcpDataSplit((char*)szSendData+13,  sizeof(szSendData)-13);  
  153.   
  154.     getchar();  
  155.     return 0;  
  156. }

三、TCP粘包分包处理(2)这段代码其实作用与TCP粘包分包处理(3)作用完全一致,不同点在于第一个在函数之前调用recv而第二个(就是这个)在函数中调用recv


TCP为什么需要进行封包解包?
        TCP采用字节流的方式,即以字节为单位传输字节序列。那么,我们recv到的就是一串毫无规则的字节流。如果要让这无规则的字节流有规则,那么,就需要我们去定义一个规则。那便是所谓的“封包规则”。

源代码打包下载:
testNetPacket.rar
封包结构是怎么样的?
        封包就像是信,信是由:信封、信内容。两部分组成。而网络封包也是由两部分组成:包头、数据。包头域是定长的,数据域是不定长的。包头必然包含两个信息:操作码、包长度。包头可能还包含别的信息,这个呢就要视乎情况去定了。操作码是该网络数据包的标识符,这就和UI里面的事件ID什么的差不多。其中,操作码有的只有一级,有的则有两级甚至多级操作码,这个的设计也要看情况去了,不过,这些底层的东西,定好了,基本就不能动了,就像房子都砌起来了,再去动地基,那就欧也了。
以下是网络数据包的伪代码:

[cpp] view plain copy
  1. struct NetPacket  
  2. {  
  3. NetPacketHeader     Header;                                                //包头  
  4. unsigned char       Data[NET_PACKET_DATA_SIZE];     /// 数据  
  5. };  


以下是包头的伪代码:


[cpp] view plain copy
  1. struct NetPacketHeader  
  2. {  
  3.     unsigned short      wDataSize;  ///< 数据包大小,包含封包头和封包数据大小  
  4.     unsigned short      wOpcode;    ///< 操作码  
  5. };  


收包中存在的一个问题(粘包,半包)
        在现实的网络情况中,网络传输往往是不可靠的,因此会有丢包之类的情况发生,对此,TCP相应的有一个重传的机制。对于接收者来说,它接收到的数据流中的数据有可能不是完整的数据包,或是只有一部分,或是粘着别的数据包,因此,接收者还需要对接收到的数据流的数据进行分包。


服务器客户端逻辑描述
        服务等待一个客户端的连接,客户端连接上了以后,服务器向客户端发送5个数据包,客户端接收服务器端的数据并解包然后做相应的逻辑处理。


需要注意的事项
1.服务器客户端是阻塞的,而不是非阻塞的套接字,这是为了简单;
2.当客户端收到了5个数据包之后,就主动和服务器断开连接,这个是硬代码;
3.阻塞套接字其实没有必要这样处理数据包,主要应用在非阻塞的套接字上;
4.此段代码只支持POD数据,不支持变长的情况;
5.各平台下字节对齐方式不一样,如Windows下默认字节对齐为4,这是此方式需要注意的。




服务器CPP代码:




[cpp] view plain copy
  1. #include "stdafx.h"  
  2. #include "TCPServer.h"  
  3.   
  4.   
  5. TCPServer::TCPServer()  
  6. : mServerSocket(INVALID_SOCKET)  
  7. {  
  8.     // 创建套接字  
  9.     mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);  
  10.     if (mServerSocket == INVALID_SOCKET)  
  11.     {  
  12.         std::cout << "创建套接字失败!" << std::endl;  
  13.         return;  
  14.     }  
  15.   
  16.   
  17.     // 填充服务器的IP和端口号  
  18.     mServerAddr.sin_family        = AF_INET;  
  19.     mServerAddr.sin_addr.s_addr    = INADDR_ANY;  
  20.     mServerAddr.sin_port        = htons((u_short)SERVER_PORT);  
  21.   
  22.   
  23.     // 绑定IP和端口  
  24.     if ( ::bind(mServerSocket, (sockaddr*)&mServerAddr, sizeof(mServerAddr)) == SOCKET_ERROR)  
  25.     {  
  26.         std::cout << "绑定IP和端口失败!" << std::endl;  
  27.         return;  
  28.     }  
  29.   
  30.   
  31.     // 监听客户端请求,最大同时连接数设置为10.  
  32.     if ( ::listen(mServerSocket, SOMAXCONN) == SOCKET_ERROR)  
  33.     {  
  34.         std::cout << "监听端口失败!" << std::endl;  
  35.         return;  
  36.     }  
  37.   
  38.   
  39.     std::cout << "启动TCP服务器成功!" << std::endl;  
  40. }  
  41.   
  42.   
  43. TCPServer::~TCPServer()  
  44. {  
  45.     ::closesocket(mServerSocket);  
  46.     std::cout << "关闭TCP服务器成功!" << std::endl;  
  47. }  
  48.   
  49.   
  50. void TCPServer::run()  
  51. {  
  52.     // 接收客户端的连接  
  53.     acceptClient();  
  54.   
  55.   
  56.     int nCount = 0;  
  57.     for (;;)  
  58.     {  
  59.         if (mAcceptSocket == INVALID_SOCKET)   
  60.         {  
  61.             std::cout << "客户端主动断开了连接!" << std::endl;  
  62.             break;  
  63.         }  
  64.   
  65.   
  66.         // 发送数据包  
  67.         NetPacket_Test1 msg;  
  68.         msg.nIndex = nCount;  
  69.         strncpy(msg.arrMessage, "[1]你好[2]你好[3]你好"sizeof(msg.arrMessage) );  
  70.         bool bRet = SendData(NET_TEST1, (const char*)&msg, sizeof(msg));  
  71.         if (bRet)  
  72.         {  
  73.             std::cout << "发送数据成功!" << std::endl;  
  74.         }  
  75.         else  
  76.         {  
  77.             std::cout << "发送数据失败!" << std::endl;  
  78.             break;  
  79.         }  
  80.   
  81.   
  82.         ++nCount;  
  83.     }  
  84. }  
  85.   
  86.   
  87. void TCPServer::closeClient()  
  88. {  
  89.     // 判断套接字是否有效  
  90.     if (mAcceptSocket == INVALID_SOCKET) return;  
  91.   
  92.   
  93.     // 关闭客户端套接字  
  94.     ::closesocket(mAcceptSocket);  
  95.     std::cout << "客户端套接字已关闭!" << std::endl;  
  96. }  
  97.   
  98.   
  99. void TCPServer::acceptClient()  
  100. {  
  101.     // 以阻塞方式,等待接收客户端连接  
  102.     int nAcceptAddrLen = sizeof(mAcceptAddr);  
  103.     mAcceptSocket = ::accept(mServerSocket, (struct sockaddr*)&mAcceptAddr, &nAcceptAddrLen);  
  104.     std::cout << "接受客户端IP:" << inet_ntoa(mAcceptAddr.sin_addr) << std::endl;  
  105. }  
  106.   
  107.   
  108. bool TCPServer::SendData( unsigned short nOpcode, const char* pDataBuffer, const unsigned int& nDataSize )  
  109. {  
  110.     NetPacketHeader* pHead = (NetPacketHeader*) m_cbSendBuf;  
  111.     pHead->wOpcode = nOpcode;  
  112.   
  113.   
  114.     // 数据封包  
  115.     if ( (nDataSize > 0) && (pDataBuffer != 0) )  
  116.     {  
  117.         CopyMemory(pHead+1, pDataBuffer, nDataSize);  
  118.     }  
  119.   
  120.   
  121.     // 发送消息  
  122.     const unsigned short nSendSize = nDataSize + sizeof(NetPacketHeader);  
  123.     pHead->wDataSize = nSendSize;  
  124.     int ret = ::send(mAcceptSocket, m_cbSendBuf, nSendSize, 0);  
  125.     return (ret > 0) ? true : false;  
  126. }  





客户端CPP代码:

[cpp] view plain copy
  1. #include "stdafx.h"  
  2. #include "TCPClient.h"  
  3.   
  4.   
  5.   
  6.   
  7. TCPClient::TCPClient()  
  8. {  
  9.     memset( m_cbRecvBuf, 0, sizeof(m_cbRecvBuf) );  
  10.     m_nRecvSize = 0;  
  11.   
  12.   
  13.     // 创建套接字  
  14.     mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);  
  15.     if (mServerSocket == INVALID_SOCKET)  
  16.     {  
  17.         std::cout << "创建套接字失败!" << std::endl;  
  18.         return;  
  19.     }  
  20.   
  21.   
  22.     // 填充服务器的IP和端口号  
  23.     mServerAddr.sin_family        = AF_INET;  
  24.     mServerAddr.sin_addr.s_addr    = inet_addr(SERVER_IP);  
  25.     mServerAddr.sin_port        = htons((u_short)SERVER_PORT);  
  26.   
  27.   
  28.     // 连接到服务器  
  29.     if ( ::connect(mServerSocket, (struct sockaddr*)&mServerAddr, sizeof(mServerAddr)))  
  30.     {  
  31.         ::closesocket(mServerSocket);  
  32.         std::cout << "连接服务器失败!" << std::endl;  
  33.         return;      
  34.     }  
  35. }  
  36.   
  37.   
  38. TCPClient::~TCPClient()  
  39. {  
  40.     ::closesocket(mServerSocket);  
  41. }  
  42.   
  43.   
  44. void TCPClient::run()  
  45. {  
  46.     int nCount = 0;  
  47.     for (;;)  
  48.     {  
  49.         // 接收数据  
  50.         int nRecvSize = ::recv(mServerSocket,  
  51.             m_cbRecvBuf+m_nRecvSize,   
  52.             sizeof(m_cbRecvBuf)-m_nRecvSize, 0);  
  53.         if (nRecvSize <= 0)  
  54.         {  
  55.             std::cout << "服务器主动断开连接!" << std::endl;  
  56.             break;  
  57.         }  
  58.   
  59.   
  60.         // 保存已经接收数据的大小  
  61.         m_nRecvSize += nRecvSize;  
  62.   
  63.   
  64.         // 接收到的数据够不够一个包头的长度  
  65.         while (m_nRecvSize >= sizeof(NetPacketHeader))  
  66.         {  
  67.             // 收够5个包,主动与服务器断开  
  68.             if (nCount >= 5)  
  69.             {  
  70.                 ::closesocket(mServerSocket);  
  71.                 break;  
  72.             }  
  73.   
  74.   
  75.             // 读取包头  
  76.             NetPacketHeader* pHead = (NetPacketHeader*) (m_cbRecvBuf);  
  77.             const unsigned short nPacketSize = pHead->wDataSize;  
  78.   
  79.   
  80.             // 判断是否已接收到足够一个完整包的数据  
  81.             if (m_nRecvSize < nPacketSize)  
  82.             {  
  83.                 // 还不够拼凑出一个完整包  
  84.                 break;  
  85.             }  
  86.   
  87.   
  88.             // 拷贝到数据缓存  
  89.             CopyMemory(m_cbDataBuf, m_cbRecvBuf, nPacketSize);  
  90.   
  91.   
  92.             // 从接收缓存移除  
  93.             MoveMemory(m_cbRecvBuf, m_cbRecvBuf+nPacketSize, m_nRecvSize);  
  94.             m_nRecvSize -= nPacketSize;  
  95.   
  96.   
  97.             // 解密数据,以下省略一万字  
  98.             //   
  99.   
  100.   
  101.             // 分派数据包,让应用层进行逻辑处理  
  102.             pHead = (NetPacketHeader*) (m_cbDataBuf);  
  103.             const unsigned short nDataSize = nPacketSize - (unsigned short)sizeof(NetPacketHeader);  
  104.             OnNetMessage(pHead->wOpcode, m_cbDataBuf+sizeof(NetPacketHeader), nDataSize);  
  105.   
  106.   
  107.             ++nCount;  
  108.         }  
  109.     }  
  110.   
  111.   
  112.     std::cout << "已经和服务器断开连接!" << std::endl;  
  113. }  
  114.   
  115.   
  116. bool TCPClient::OnNetMessage( const unsigned short& nOpcode,   
  117.                              const char* pDataBuffer, unsigned short nDataSize )  
  118. {  
  119.     switch (nOpcode)  
  120.     {  
  121.     case NET_TEST1:  
  122.         {  
  123.             NetPacket_Test1* pMsg = (NetPacket_Test1*) pDataBuffer;  
  124.             return OnNetPacket(pMsg);  
  125.         }  
  126.         break;  
  127.   
  128.   
  129.     default:  
  130.         {  
  131.             std::cout << "收取到未知网络数据包:" << nOpcode << std::endl;  
  132.             return false;  
  133.         }  
  134.         break;  
  135.     }  
  136. }  
  137.   
  138.   
  139. bool TCPClient::OnNetPacket( NetPacket_Test1* pMsg )  
  140. {  
  141.     std::cout << "索引:" << pMsg->nIndex << "  字符串:" << pMsg->arrMessage << std::endl;  
  142.     return true;  
  143. }  

转载自https://blog.****.net/rock_joker/article/details/60885270