TCP编程之二 粘包分包处理逻辑
基于TCP的网络编程中, 数据传输是基于连接的,所以当网络出现堵塞或者发送频率过高的时候,就会出现粘包的情况。
粘包就是并不是一个接收对应一个发送,有可能一个接收对应多个发送,也可能一个接收少于一个发送。
由于我们在网络编程中,经常以对象作为发送的单元,所以接受端必须对粘包做处理,还原原来的对象。
下图说明了接受端接收到数据的各种情况:
当然,接收到第一种情况是最理想的,也不须处理。本文针对2 3 4情况做处理。
算法解析:
首先有一个对象用于保存上次未能处理的数据,和上次为处理数据的长度。
1. 将本次接收到的数据拼接到上一次未处理数据后面,为未处理数据。
2. 判断未处理数据长度是否大于包头,
若小于包头,直接退出(包头保存长度信息) , 否则转3。
3. 根据包头判断对象大小是否大于未处理数据长度,若是转3, 否则保存未处理数据退出。
4. 截出第一个对象进行处理,剩下的数据重新保存到未处理对象,继续转2循环.
- // 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采用字节流的方式,即以字节为单位传输字节序列。那么,我们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;
- }