结构体传输& TCP粘包处理

    xiaoxiao2021-03-25  205

    一、网络传输struct类型数据

    在网络通讯过程中往往涉及一些有关联的参数传递,例如结构体之类的。对于结构体其实方法挺简单,由于结构体对象在内存中分配的空间都是连续的,所以可以将整个结构体直接转化成字符串发送,到了接收方再将这个字符串还原成结构体就可以了。

    网络传输struct数据的约束有两个:

    约束一、就是结构体的大小必须是固定的,不能含有可变大小数据,例如CString、string之类的数据。换句话说,结构体所包含的数据必须是C++基本类型数据以及这些基本类型数据所形成固定大小的数组。

    约束二、就是传接两方结构体定义必须一模一样,包括数据声明次序。如果要发送的结构体包含“#pragma pack (n)”之类的东西(具体可看http://blog.csdn.net/21aspnet/article/details/6730124),则接收方在定义此结构体时也要使用“#pragma pack (n)”声明。传接之所以能够成功是因为结构体数据的内存区域连续性所保证的

     

    本来嘛在C/C++中所有数据究其本质都是字节类型,只是在表现时各自不同罢了,所以只要能找到合适的转换为字节类型数据的途径就OK了。而字节类型和char类型一样都是一个字节长度,所以问题又等同于找一条合适途径,将信息转换为固定长度char数组类型。

     

    下面举例说明

    1、结构体数据全部都是数组

    typedef struct _tag_user_info_ { char cUserID[20]; char cUserSex[10]; char cUserName[18]; char cUserNativePlace[50]; } UserData;   //发送方:创建一个对象并初始化各个参数,然后发送。 UserData sendUser; memcpy ( sendUser.cUserID, "412902198312120311",sizeof("412902198312120311")); memcpy ( sendUser.cUserSex, "male",sizeof("male")); memcpy ( sendUser.cUserName, "JianYa.Lee",sizeof("JianYa.Lee")); memcpy ( sendUser.cUserNativePlace, "Asia. P.R.C .HeNan-DengZhouShi", sizeof("Asia. P.R.C.HeNan-DengZhouShi") ); send ( m_oSendSocket, (char*)&sendUser,sizeof(UserData), 0 ); 需要注意的地方:send函数的第三个参数,也就是发送数据长度必需是结构体的大小,这样发送方就已经将这个sendUser对象以字符串的形式发送出去了,剩下的工作就由接收方来完成了

     

      接收方:首先也必须有UserData这个结构体类型定义。其次,首先定义一个充分大char类型数组,用于接收网络发送数据。然后将接收到的数据用memcpy函数完成强转即可。

    // 定义的char数组足够大 charcRcvBuffer[1024] = {0}; // 定义一个UserData对象, 用于容纳转换信息 UserData recvUser; recv( m_RcvSocket, cRcvBuffer, sizeof(cRcvBuffer),0 ); // 强转, 请注意sizeof的内容 memcpy( &recvUser, cRcvBuffer, sizeof(UserData) );

    这样得到的recvUser对象里的数据与sendUser相同了。

    2、结构体数据没有包含数组

    // 发送方:创建struct结构体 typedef struct _tag_other_data_ { INT32 nValue; char cValue; bool blValue; float fValue; double dValue; short sValue; } SecondData; // 定义结构体对象,并初始化 SecondData oScdData; // 初始化数据内容 oScdData.blValue = true; oScdData.cValue = 'h'; oScdData.dValue = 0.1234567; oScdData.fValue =3.14159f; oScdData.nValue = 123456; oScdData.sValue = 0x1024; // 注意sizeof内容 send(m_oSendSocket, (char*)&oScdData,sizeof(SecondData), 0); 接收方:首先定义SecondData结构体,数据类型、声明次序需完全一样;其次声明一个足够大的char类型数组;最后强转。 // 定义char类型数组 charcRcvBuffer[1024] = {0}; SecondData oRcvData; // 注意sizeof内容 recv(m_oRcvSocket, cRcvBuffer, sizeof(cRcvBuffer), 0); // 强制转换, 注意sizeof内容 memcpy(&oRcvData, cRcvBuffer, sizeof(SecondData));

    接收方:首先定义SecondData结构体,数据类型、声明次序需完全一样;其次声明一个足够大的char类型数组;最后强转。

    // 定义char类型数组 charcRcvBuffer[1024] = {0}; SecondData oRcvData; // 注意sizeof内容 recv(m_oRcvSocket, cRcvBuffer, sizeof(cRcvBuffer), 0); // 强制转换, 注意sizeof内容 memcpy(&oRcvData, cRcvBuffer, sizeof(SecondData));

    3、多个结构体数据的传接

    发送方:

    struct structA { INT32 nValue; char cValue; }; struct structB { bool blValue; short sValue; }; struct structC { float fValue; char cValue; unsigned long unValue; }; // 三个结构体定义各不相同,现在要给它们建立一个统一的传接模式,此时可以考虑使用联合union,外加一个类型指示。 typedef struct _tag_unification_data_ { // 用于指示结构体类型, 比如IS_STRUCT_A就代表structA、 // IS_STRUCT_B就代表struct_B、 // IS_STRUCT_C就代表structC INT32 nStructType; // 每次传送的是三种struct中的一种 union { struct structA aData; struct structB bData; struct structC cData; }; } PACKETDATA; // 结构体类型标识 enum{IS_STRUCT_A, IS_STRUCT_B, IS_STRUCT_C}; // 定义结构体对象,并初始化 PACKETDATA oMyData; // 发送structA类型数据 oMyData.nStructType = IS_STRUCT_A; oMyData.aData.cValue = 'g'; oMyData.aData.nValue = 130; // 注意后面的sizeof内容 send(oSendSocket, (char*)&oMyData,sizeof(PACKETDATA), 0);

    接收方:

    首先必需由PACKETDATA一样的定义;

    其次,定义一个足够大的char数组;

    最后完成强转,在使用的时候进行具体类型判断即可。

    // 定义char类型数组 charcRcvBuffer[1024] = {0}; PACKETDATA oRcvData; // 注意sizeof内容 recv(m_oRcvSocket, cRcvBuffer, sizeof(cRcvBuffer), 0); // 强制转换, 注意sizeof内容 memcpy(&oRcvData, cRcvBuffer, sizeof(PACKETDATA)); // 在使用时进行具体类型判断 switch (oRcvData.nStructType) { caseIS_STRUCT_A: // structA类型数据 break; caseIS_STRUCT_B: // structB类型数据 break; caseIS_STRUCT_C: // structC类型数据 break; }

    二、TCP粘包分包处理(1)

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

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

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

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

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

    算法解析:

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

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

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

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

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

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

    [cpp]  view plain  copy // TcpDataSplit.cpp : 定义控制台应用程序的入口点。   //      #include "stdafx.h"   #include <stdio.h>   #include <stdlib.h>   #include <string.h>      #define MAX_NETPACK_SIZE    10000   #define MAX_DATA_SIZE           4086      /* 数据包头类型 */   struct NetDataHeader_t   {       int nDataType;                                          //数据包类型,标识对应的对象类型       int nDataSize;                                          //数据包中szData真实数据的长度   };      /*  数据包类型 */   struct NetDataBase_t   {       NetDataHeader_t  dataHeader;            //数据包头       char     szData[MAX_DATA_SIZE];             //真实数据   };      /**      其实NetDataBase_t是基础类型,由此我们可以延伸出很多子类型,      所以我们要清楚,每个类型的长度是不一样的,不都是sizeof(NetDataBase_t),      就是各个类型对象大小不一样,比如:      在派生结构体中,NetDataPeople_t和NetDataSchool_t是两个各异的结构体,      但他们都有相关的Header部分指明结构体类型和长度。  */   struct NetDataPeople_t   {       NetDataHeader_t dataHeader;        int     nAge;       char    szName[10];   };      struct NetDataSchool_t   {       NetDataHeader_t dataHeader;        char    szShoolName[20];       char    szShoolAddress[30];   };      /**      处理整理好的对象。  */   bool HandleNetPack(NetDataHeader_t* pDataHeader);         bool TcpDataSplit(const char* szRecNetData, int nRecSize)   {       /**          对于szLastSaveData, nRemainSize,为了简单,本例子只          作为静态变量使用,因此只限于一个socket的数据接收,          假如要同时处理多个socket数据,请放在对应容器里保存      */       static char szLastSaveData[MAX_NETPACK_SIZE];       static int nRemainSize = 0;       static bool bFirst = true;          if (bFirst)       {           memset(szLastSaveData, 0, sizeof(szLastSaveData));           bFirst = false;       }          /* 本次接收到的数据拼接到上次数据 */       memcpy( (char*)(szLastSaveData+nRemainSize), szRecNetData, nRecSize );       nRemainSize = nRecSize + nRemainSize;          /* 强制转换成NetDataPack指针 */       NetDataHeader_t* pDataHead = (NetDataHeader_t*)szLastSaveData;          /**         核心算法       */       while ( nRemainSize >sizeof(NetDataHeader_t) &&                   nRemainSize >= pDataHead->nDataSize +sizeof(NetDataHeader_t) )       {               HandleNetPack(pDataHead);               int  nRecObjectSize = sizeof(NetDataHeader_t) + pDataHead->nDataSize;        //本次收到对象的大小               nRemainSize -= nRecObjectSize ;                            pDataHead = (NetDataHeader_t*)( (char*)pDataHead + nRecObjectSize );        //移动下一个对象头       }              /* 余下数据未能组成一个对象,先保存起来 */       if (szLastSaveData != (char*)pDataHead)       {           memmove(szLastSaveData, (char*)pDataHead, nRemainSize);           memset( (char*)( szLastSaveData+nRemainSize), 0, sizeof(szLastSaveData)-nRemainSize );       }              return true;   }         /**      处理整理好的对象。  */   bool HandleNetPack(NetDataHeader_t* pDataHeader)   {       //处理数据包       if  (pDataHeader->nDataType == 1)       {           NetDataPeople_t* pPeople = (NetDataPeople_t*)pDataHeader;           printf("收到People对象,Age:%d, Name:%s\n", pPeople->nAge, pPeople->szName);       }       else if (pDataHeader->nDataType == 2)       {           NetDataSchool_t* pSchool = (NetDataSchool_t*)pDataHeader;           printf("收到School对象,SchoolName:%s, SchoolAddress:%s\n", pSchool->szShoolName, pSchool->szShoolAddress);       }          return true;   }      int _tmain(int argc, _TCHAR* argv[])   {       /* 本例子以两个对象作为接收到的数据 */       NetDataPeople_t  people;       people.dataHeader.nDataSize = sizeof(people) - sizeof(NetDataHeader_t);       people.dataHeader.nDataType = 1;       people.nAge = 20;       sprintf(people.szName, "Jim");      //real data          NetDataSchool_t  school;       school.dataHeader.nDataSize = sizeof(school) - sizeof(NetDataHeader_t);       school.dataHeader.nDataType = 2;       sprintf(school.szShoolName, "清华大学");        //real data       sprintf(school.szShoolAddress, "北京市北京路");       //real data          /* 将两个对象数据合并到一个地址里面以便重现粘包 */       char szSendData[sizeof(people)+sizeof(school)];       memcpy(szSendData,  (char*)&people,  sizeof(people));       memcpy(szSendData+sizeof(people),  (char*)&school,  sizeof(school));          //这里进行收数据操作,这里省略。。。          /**          特意设置粘包:          1.第一次只发送3个字节,还不足以构建包头          2.第二次发送10个字节,总共13个,但第一个对象大小是8+14=18;因此第一个对象people还没收满          3.第三次发送剩下的全部,第一个对象剩下的部分与第二个对象粘在一起,验证处理      */       TcpDataSplit((char*)szSendData, 3);   //在这里传递值3为recv的返回值。比如int i = recv(); TcpDataSplit((char*)szSendData, i);     TcpDataSplit((char*)szSendData, recv(....))     TcpDataSplit((char*)szSendData+3,  10);       TcpDataSplit((char*)szSendData+13,  sizeof(szSendData)-13);          getchar();       return 0;   }

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

    TCP为什么需要进行封包解包?         TCP采用字节流的方式,即以字节为单位传输字节序列。那么,我们recv到的就是一串毫无规则的字节流。如果要让这无规则的字节流有规则,那么,就需要我们去定义一个规则。那便是所谓的“封包规则”。 源代码打包下载: testNetPacket.rar 封包结构是怎么样的?         封包就像是信,信是由:信封、信内容。两部分组成。而网络封包也是由两部分组成:包头、数据。包头域是定长的,数据域是不定长的。包头必然包含两个信息:操作码、包长度。包头可能还包含别的信息,这个呢就要视乎情况去定了。操作码是该网络数据包的标识符,这就和UI里面的事件ID什么的差不多。其中,操作码有的只有一级,有的则有两级甚至多级操作码,这个的设计也要看情况去了,不过,这些底层的东西,定好了,基本就不能动了,就像房子都砌起来了,再去动地基,那就欧也了。 以下是网络数据包的伪代码: struct NetPacket { NetPacketHeader Header; //包头 unsigned char Data[NET_PACKET_DATA_SIZE]; /// 数据 }; 以下是包头的伪代码: struct NetPacketHeader { unsigned short wDataSize; ///< 数据包大小,包含封包头和封包数据大小 unsigned short wOpcode; ///< 操作码 }; 收包中存在的一个问题(粘包,半包)         在现实的网络情况中,网络传输往往是不可靠的,因此会有丢包之类的情况发生,对此,TCP相应的有一个重传的机制。对于接收者来说,它接收到的数据流中的数据有可能不是完整的数据包,或是只有一部分,或是粘着别的数据包,因此,接收者还需要对接收到的数据流的数据进行分包。 服务器客户端逻辑描述         服务等待一个客户端的连接,客户端连接上了以后,服务器向客户端发送5个数据包,客户端接收服务器端的数据并解包然后做相应的逻辑处理。 需要注意的事项 1.服务器客户端是阻塞的,而不是非阻塞的套接字,这是为了简单; 2.当客户端收到了5个数据包之后,就主动和服务器断开连接,这个是硬代码; 3.阻塞套接字其实没有必要这样处理数据包,主要应用在非阻塞的套接字上; 4.此段代码只支持POD数据,不支持变长的情况; 5.各平台下字节对齐方式不一样,如Windows下默认字节对齐为4,这是此方式需要注意的。 服务器CPP代码: #include "stdafx.h" #include "TCPServer.h" TCPServer::TCPServer() : mServerSocket(INVALID_SOCKET) { // 创建套接字 mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (mServerSocket == INVALID_SOCKET) { std::cout << "创建套接字失败!" << std::endl; return; } // 填充服务器的IP和端口号 mServerAddr.sin_family = AF_INET; mServerAddr.sin_addr.s_addr = INADDR_ANY; mServerAddr.sin_port = htons((u_short)SERVER_PORT); // 绑定IP和端口 if ( ::bind(mServerSocket, (sockaddr*)&mServerAddr, sizeof(mServerAddr)) == SOCKET_ERROR) { std::cout << "绑定IP和端口失败!" << std::endl; return; } // 监听客户端请求,最大同时连接数设置为10. if ( ::listen(mServerSocket, SOMAXCONN) == SOCKET_ERROR) { std::cout << "监听端口失败!" << std::endl; return; } std::cout << "启动TCP服务器成功!" << std::endl; } TCPServer::~TCPServer() { ::closesocket(mServerSocket); std::cout << "关闭TCP服务器成功!" << std::endl; } void TCPServer::run() { // 接收客户端的连接 acceptClient(); int nCount = 0; for (;;) { if (mAcceptSocket == INVALID_SOCKET) { std::cout << "客户端主动断开了连接!" << std::endl; break; } // 发送数据包 NetPacket_Test1 msg; msg.nIndex = nCount; strncpy(msg.arrMessage, "[1]你好[2]你好[3]你好", sizeof(msg.arrMessage) ); bool bRet = SendData(NET_TEST1, (const char*)&msg, sizeof(msg)); if (bRet) { std::cout << "发送数据成功!" << std::endl; } else { std::cout << "发送数据失败!" << std::endl; break; } ++nCount; } } void TCPServer::closeClient() { // 判断套接字是否有效 if (mAcceptSocket == INVALID_SOCKET) return; // 关闭客户端套接字 ::closesocket(mAcceptSocket); std::cout << "客户端套接字已关闭!" << std::endl; } void TCPServer::acceptClient() { // 以阻塞方式,等待接收客户端连接 int nAcceptAddrLen = sizeof(mAcceptAddr); mAcceptSocket = ::accept(mServerSocket, (struct sockaddr*)&mAcceptAddr, &nAcceptAddrLen); std::cout << "接受客户端IP:" << inet_ntoa(mAcceptAddr.sin_addr) << std::endl; } 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; } 客户端CPP代码: #include "stdafx.h" #include "TCPClient.h" TCPClient::TCPClient() { memset( m_cbRecvBuf, 0, sizeof(m_cbRecvBuf) ); m_nRecvSize = 0; // 创建套接字 mServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (mServerSocket == INVALID_SOCKET) { std::cout << "创建套接字失败!" << std::endl; return; } // 填充服务器的IP和端口号 mServerAddr.sin_family = AF_INET; mServerAddr.sin_addr.s_addr = inet_addr(SERVER_IP); mServerAddr.sin_port = htons((u_short)SERVER_PORT); // 连接到服务器 if ( ::connect(mServerSocket, (struct sockaddr*)&mServerAddr, sizeof(mServerAddr))) { ::closesocket(mServerSocket); std::cout << "连接服务器失败!" << std::endl; return; } } TCPClient::~TCPClient() { ::closesocket(mServerSocket); } void TCPClient::run() { int nCount = 0; for (;;) { // 接收数据 int nRecvSize = ::recv(mServerSocket, m_cbRecvBuf+m_nRecvSize, sizeof(m_cbRecvBuf)-m_nRecvSize, 0); if (nRecvSize <= 0) { std::cout << "服务器主动断开连接!" << std::endl; break; } // 保存已经接收数据的大小 m_nRecvSize += nRecvSize; // 接收到的数据够不够一个包头的长度 while (m_nRecvSize >= sizeof(NetPacketHeader)) { // 收够5个包,主动与服务器断开 if (nCount >= 5) { ::closesocket(mServerSocket); break; } // 读取包头 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); ++nCount; } } std::cout << "已经和服务器断开连接!" << std::endl; } bool TCPClient::OnNetMessage( const unsigned short& nOpcode, const char* pDataBuffer, unsigned short nDataSize ) { switch (nOpcode) { case NET_TEST1: { NetPacket_Test1* pMsg = (NetPacket_Test1*) pDataBuffer; return OnNetPacket(pMsg); } break; default: { std::cout << "收取到未知网络数据包:" << nOpcode << std::endl; return false; } break; } } bool TCPClient::OnNetPacket( NetPacket_Test1* pMsg ) { std::cout << "索引:" << pMsg->nIndex << " 字符串:" << pMsg->arrMessage << std::endl; return true; }参考了http://www.cppblog.com/tx7do/archive/2011/05/04/145699.html 参考了http://blog.csdn.net/luoti784600/article/details/12646405
    转载请注明原文地址: https://ju.6miu.com/read-7651.html

    最新回复(0)