男儿欲遂平生志,五经勤向窗前读。这篇文章主要讲述从一次Windows网络编程排错经历中得出的一个可靠拆包算法相关的知识,希望能为你提供帮助。
作者:朱金灿
前段时间,维护一个网络程序。客户反映我们的系统有时接收不到来自任务管理系统的socket字符串,存在丢失数据的问题。我看了一下代码(代码是别人写的),发现系统的代码写得有问题。原来系统的代码是开辟一个大的接收缓冲区,试图一下把整个数据包接收过来。实际上接收网络数据包面临一个拆包的问题,可靠的拆包的方式应是进行分段接收,然后把数据拼接起来。如何分段接收,这里大有讲究,且容我结合我的排错经历细细道来。
开始我考虑的一种算法是:
开始循环接收数据,设定每次接收100个字节(我能确保数据包都大于100个字节),对下面各种情况进行判断:
a.如果实际接收字节数为0,直接退出.
b.如果实际接收字节数小于100,就可能有两种情况:
(1)如果是第一次接收,就退出
(2)如果是最后一次接收(比如包的大小为320,最后接收的20个字节),就把数据接收过来,并退出
c.如果实际接收字节数等于100,首先判断是否是第一次接收,若是解析出包的大小(判断包头的第八个字节),然后进行接收,若不是就直接接收.
3.循环进行进行第二步,直接接收完整个包。
【从一次Windows网络编程排错经历中得出的一个可靠拆包算法】我这样设计是基于我对TCP协议的理解。我以为TCP协议能保证我每次都能接收到100个字节。我这样修改系统之后用户依然反映存在丢失数据的问题。于是我判断最有可能的情况就是第一次就接收不到100个字节从而导致程序退出。为证实我这个判断,我采用把接收到字符串都写进日志文件的办法。通过分析日志文件证实了我的判断,在丢失数据的一种情况就是第一次只接收了63个字节。这促使我更深刻地理解了TCP协议的特点。我认为TCP协议具有以下特点(大家认为不正确欢迎指出):
1.可靠性。就是甲给乙传300个字节,TCP协议能保证这300个字节一个不少地传给乙。
2.流特性。你可以想象TCP协议下的网络转输就是一条河流。这条“河流”的“河水”不是平坦的,而是起伏不平的,具体就是说你在客户端开辟一个100个字节的接收缓冲区,每次你实际接收的字节数有可能是0到100。
3.顺序性。TCP协议保证后面服务器端发给客户端的数据不会跑到前面发的数据去,所有接收数据的顺序都按照发送顺序来。
理解了TCP协议的特点,我重新设计了数据包拆包算法。一般数据包对应程序逻辑中的一个结构体,该结构体一般要定义一个数据包长度的成员变量(为保证数据包接收的完整性),而且要将该变量尽可能排在数据包前面。我把这个算法分为两步:
第一步解析数据包的长度
第二步根据数据包的长度接收数据
具体算法是:
第一步解析数据包的长度
在接收的时候,应该先指定接收8字节(假设包长度的变量所占的字节数为8),并判断返回值,如果实际接收到的数据不足8字节,应调整缓冲区指针,继续接收后面的数据,例如只收到2字节,应该将缓冲区指针加4,在执行接收,指定接收6字节,然后再判断返回值,重复上面步骤,直到收齐8字节为止。收齐8字节立即解析并保存起来。
第二步根据数据包的长度接收数据
1.设定接收缓冲区为整个包的长度.
2.开始循环接收数据,
a.如果实际接收字节数为0,直接退出.
b.如果实际接收字节数不为0,就直接接收,同时将待接收数据长度(初始化为整个包的长度)减去本次实际接收的字节数。
3.循环进行进行第二步,直接接收完整个包(以待接收数据长度为0进行判断).
值得注意的是,在各次接收过程中,如果接收函数返回0,表示连接已断开,如果返回值为-1,表示出现了错误,应根据具体情况来做出响应的处理。
相关源码为:
- BOOL GlocalReceiveMsg(CMIClientSocket* pClientSocket,BYTE** pResultBuf,int& iResultLen)
- // 待接收的数据包的长度
- intnLeftDataLen = 0;
- // 数据包体长度
- intnPackBodyLen = 0;
- // 外部接收缓冲区
- BYTE* pRstbuffer = NULL;
- // 接收函数一次实际收到的数据长度
- int nRevOnceLen = 0;
- // 包头缓冲区
- BYTE PackLenBuf[STDATABAND_SIZE];
- // STDATABAND_SIZE为网络数据包头,具体数值为12
- int nRecPackLen = STDATABAND_SIZE;
- // 初始化实际接收的长度为0
- iResultLen = 0;
- // 接收包头数据
- do
- // Receive函数第一个参数为缓冲区指针,第二个参数为缓冲区长度,返回值为
- // 实际接收的字节数。注意这里有缓冲区指针的移动和缓冲区长度的变化,变化规律就是
- // 首先设定缓冲区长度为STDATABAND_SIZE(即12),若实际接收字节数为4,那么第二
- // 次设定缓冲区指针移动4个字节,长度为12-4=8,照此方法循环接收
- nRevOnceLen = pClientSocket-> Receive(PackLenBuf+STDATABAND_SIZE-nRecPackLen,nRecPackLen);
- if(result< STDATABAND_SIZE)
- if(nRevOnceLen == SOCKET_ERROR)
- AfxMessageBox("接收数据失败");
- return FALSE;
- else if(0 == nRevOnceLen)
- pClientSocket-> Close();
- return FALSE;
- else
- nRecPackLen = nRecPackLen - nRevOnceLen;
- else
- break;
- while(nRecPackLen!=0);
- // 解析数据包头,获取数据包长度。数据包体长度(数据包除开包头的长度)放在包头的
- // 第DATA_LENGTH_INDEX字节。
- memcpy(& nPackBodyLen,PackLenBuf+DATA_LENGTH_INDEX,sizeof(DWORD));
- pRstbuffer = new BYTE[nPackBodyLen +STDATABAND_SIZE];
- nLeftDataLen = nPackBodyLen +STDATABAND_SIZE;
- // 拷贝包头数据到外部缓冲区
- memcpy(pRstbuffer,PackLenBuf,STDATABAND_SIZE);
- // 设置剩余接收的数据长度为包体长度
- nLeftDataLen -=STDATABAND_SIZE;
- // 若包体长度为0,就退出
- if(nLeftDataLen ==0)
- return TRUE;
- // 实际接受数据的长度加上包头长度,因为已经成功接收包头
- iResultLen = iResultLen + STDATABAND_SIZE;
- do
- // 循环接收包体数据,其原理和接收包头数据类似,
- //注意这里的缓冲区指针pRstbuffer是向前偏移的
- nRevOnceLen = pClientSocket-> Receive(pRstbuffer + iResultLen, nLeftDataLen);
- iResultLen = iResultLen + nRevOnceLen;
- if(nRevOnceLen == SOCKET_ERROR)
- AfxMessageBox("接收数据失败");
- return FALSE;
- else if(0 == nRevOnceLen)
- pClientSocket-> Close();
- return FALSE;
- else
- // 现在剩余接收的长度为上次剩余接收的长度减去本次实际接受的长度
- nLeftDataLen -= nRevOnceLen;
- // 假如剩余接收的长度为0,表示完全接收整个包,就退出循环
- if(nLeftDataLen ==0)
- break;
- while(true);
- // 赋值给外部缓冲区指针
- *pResultBuf = pRstbuffer;
- return TRUE;
推荐阅读
- Windows Socket网络编程学习笔记一
- 跟着动画学Go数据结构之插入排序 #私藏项目实操分享#
- EasyCVR启动失败并报错LibEasySnap.dll(The specified module could not be found)
- 滑动冲突问题,触摸事件拦截处理
- #yyds干货盘点#-设计模式分享-组合模式
- 重要的ui组件——Behavior
- gitlab 搭建go modules私有仓库
- Django+uwsgi在linux和windows上的部署
- Android 控件架构与自定义控件详解