在网络通讯过程中往往涉及一些有关联的参数传递,例如结构体之类的。对于结构体其实方法挺简单,由于结构体对象在内存中分配的空间都是连续的,所以可以将整个结构体直接转化成字符串发送,到了接收方再将这个字符串还原成结构体就可以了。
网络传输struct数据的约束有两个:
约束一、就是结构体的大小必须是固定的,不能含有可变大小数据,例如CString、string之类的数据。换句话说,结构体所包含的数据必须是C++基本类型数据以及这些基本类型数据所形成固定大小的数组。
约束二、就是传接两方结构体定义必须一模一样,包括数据声明次序。如果要发送的结构体包含“#pragma pack (n)”之类的东西(具体可看http://blog.csdn.net/21aspnet/article/details/6730124),则接收方在定义此结构体时也要使用“#pragma pack (n)”声明。传接之所以能够成功是因为结构体数据的内存区域连续性所保证的
本来嘛在C/C++中所有数据究其本质都是字节类型,只是在表现时各自不同罢了,所以只要能找到合适的转换为字节类型数据的途径就OK了。而字节类型和char类型一样都是一个字节长度,所以问题又等同于找一条合适途径,将信息转换为固定长度char数组类型。
下面举例说明
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相同了。
接收方:首先定义SecondData结构体,数据类型、声明次序需完全一样;其次声明一个足够大的char类型数组;最后强转。
// 定义char类型数组 charcRcvBuffer[1024] = {0}; SecondData oRcvData; // 注意sizeof内容 recv(m_oRcvSocket, cRcvBuffer, sizeof(cRcvBuffer), 0); // 强制转换, 注意sizeof内容 memcpy(&oRcvData, cRcvBuffer, sizeof(SecondData));
发送方:
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的网络编程中, 数据传输是基于连接的,所以当网络出现堵塞或者发送频率过高的时候,就会出现粘包的情况。
粘包就是并不是一个接收对应一个发送,有可能一个接收对应多个发送,也可能一个接收少于一个发送。
由于我们在网络编程中,经常以对象作为发送的单元,所以接受端必须对粘包做处理,还原原来的对象。
下图说明了接受端接收到数据的各种情况:
当然,接收到第一种情况是最理想的,也不须处理。本文针对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; }