对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了.

一.为什么基于TCP的通讯程序需要进行封包和拆包.

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.

假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).

A.先接收到data1,然后接收到data2.

B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.

C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.

D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

二.为什么会出现B.C.D的情况.

"粘包"可发生在发送端也可发生在接收端.

1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.象C和D的情况就有可能是Nagle算法造成的.

2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据.

三.怎样封包和拆包.

最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决.这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠.后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决象B的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷(但是象FTP等协议采用的就是应答方式).再后来就是对数据包进行封包和拆包的操作.

封包:

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

对于拆包目前我最常用的是以下两种方式.

1.动态缓冲区暂存方式.之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.

大概过程描述如下:

A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

B,当接收到数据时首先把此段数据存放在缓冲区中.

C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

D,根据包头数据解析出里面代表包体长度的变量.

E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

这种方法有两个缺点.1.为每个连接动态分配一个缓冲区增大了内存的使用.2.有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.这种拆包的改进方法会解决和完善部分缺点.

下面给出相关代码.

先看包头结构定义

#pragma pack(push,1) //开始定义数据包, 采用字节对齐方式

/*----------------------包头---------------------*/

typedef struct tagPACKAGEHEAD

{

BYTE Version;

WORD Command;

WORD nDataLen;//包体的长度

}PACKAGE_HEAD;

#pragma pack(pop) //结束定义数据包, 恢复原来对齐方式

然后看存放数据和"取"数据函数.

/*****************************************************************************

Description:添加数据到缓存

Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度

Return: 如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回TRUE。

******************************************************************************/

BOOL CDataBufferPool::AddBuff( char *pBuff, int nLen )

{

m_cs.Lock();///临界区锁

if ( nLen < 0 )

{

m_cs.Unlock();

return FALSE;

}

if ( nLen <= GetFreeSize() )///判断剩余空间是否足够存放nLen长的数据

{

memcpy(m_pBuff + m_nOffset, pBuff, nLen);

m_nOffset += nLen;

}

else///若不够则扩充原有的空间

{

char *p = m_pBuff;

m_nSize += nLen*2;//每次增长2*nLen

m_pBuff = new char[m_nSize];

memcpy(m_pBuff,p,m_nOffset);

delete []p;

memcpy(m_pBuff + m_nOffset, pBuff, nLen);

m_nOffset += nLen;

m_cs.Unlock();

return FALSE;

}

m_cs.Unlock();

return TRUE;

}

/*****************************************************************************

Description:获取一个完整的包

Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度

Return: 1、当前缓冲区不够一个包头的数据 2、当前缓冲区不够一个包体的数据

******************************************************************************/

int CDataBufferPool::GetFullPacket( char *Buf, int& nLen )

{

m_cs.Lock();

if ( m_nOffset < m_PacketHeadLen )//当前缓冲区不够一个包头的数据

{

m_cs.Unlock();

return 1;

}

PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;

if( (m_nOffset-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据

{

m_cs.Unlock();

return 2;

}

//判断包的合法性

/* int IsIntegrallity = ValidatePackIntegrality(p);

if( IsIntegrallity != 0 )

{

m_cs.Unlock();

return IsIntegrallity;

}

*/

nLen = m_PacketHeadLen+p->nDataLen;

memcpy( Buf, m_pBuff, nLen );

m_nOffset -= nLen;

memcpy( m_pBuff, m_pBuff+nLen, m_nOffset );

m_cs.Unlock();

return 0;

}

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).

第2种拆包方式会解决这两个问题.

环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.用代码来说明.注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改.

int CCircularBufferPool::PutData(TCHAR *pData, int len)

{

if( len <= 0 )

return 1;

EnterCriticalSection(&m_cs);

while (IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据

{

BufferResize(len);///若不够,则扩充缓冲区.

}

if (IsIndexOverFlow(len))///判断"尾"指针的位置.

{

int FirstCopyLen = m_iBufSize-m_iTailPos;

int SecondCopyLen = len - FirstCopyLen;

CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);

if (SecondCopyLen)

{

CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);

m_iTailPos = SecondCopyLen;

}

else

m_iTailPos = 0;

}

else

{

CopyMemory(m_pBuffer+m_iTailPos, pData, len);

m_iTailPos += len;

}

LeaveCriticalSection(&m_cs);

return 0;

}

void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)

{

if (len < m_iBufSize-m_iHeadPos)

{

CopyMemory(pData, m_pBuffer+m_iHeadPos, len);

if(Delete==true)

m_iHeadPos += len;

}

else

{

int fc, sc;

fc = m_iBufSize-m_iHeadPos;

sc = len - fc;

CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);

if (sc) CopyMemory(pData+fc, m_pBuffer, sc);

if(Delete==true)

m_iHeadPos = sc;

if(m_iHeadPos >= m_iBufSize)

m_iHeadPos = 0;

}

}

//

//进行自定义包的解析

//

int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )

{

EnterCriticalSection(&m_cs);

if( GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据

{

LeaveCriticalSection(&m_cs);

return 1;

}

GetData(Buf,m_PacketHeadLen,false);

PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;

if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据

{

LeaveCriticalSection(&m_cs);

return 2;

}

//判断包的合法性

int IsIntegrallity = ValidatePackIntegrality(p);

if( IsIntegrallity != 0 )

{

LeaveCriticalSection(&m_cs);

return IsIntegrallity;

}

GetData(Buf,m_PacketHeadLen+p->nDataLen,true);

nLen = m_PacketHeadLen+p->nDataLen;

LeaveCriticalSection(&m_cs);

return 0;

}

2.利用底层的缓冲区来进行拆包

由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了.另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据.利用这两个条件我们就可以对第一种方法进行优化了.

对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据.

相关代码如下:

char PackageHead[1024];

char PackageContext[1024*20];

int len;

PACKAGE_HEAD *pPackageHead;

while( m_bClose == false )

{

memset(PackageHead,0,sizeof(PACKAGE_HEAD));

len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));

if( len == SOCKET_ERROR )

{

break;

}

if(len == 0)

{

break;

}

pPackageHead = (PACKAGE_HEAD *)PackageHead;

memset(PackageContext,0,sizeof(PackageContext));

if(pPackageHead->nDataLen>0)

{

len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);

}

}

m_TcpSock是一个封装了SOCKET的类的变量,其中的ReceiveSize用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才返回.

int winSocket::ReceiveSize( char* strData, int iLen )

{

if( strData == NULL )

return ERR_BADPARAM;

char *p = strData;

int len = iLen;

int ret = 0;

int returnlen = 0;

while( len > 0)

{

ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );

if ( ret == SOCKET_ERROR || ret == 0 )

{

return ret;

}

len -= ret;

returnlen += ret;

}

return returnlen;

}

对于非阻塞的SOCKET,比如完成端口,我们可以提交接收包头长度的数据的请求,当GetQueuedCompletionStatus返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求.当接收包体时,采用类似的方法.

下面给出相关代码

enum IOType

{

IOInitialize,

IORead,

IOWrite,

IOIdle

};

class OVERLAPPEDPLUS

{

public:

OVERLAPPED   m_ol;

IOType    m_ioType;

bool         m_bIsPackageHead;//当前接收的数据是否是包头数据。

int          m_count;

WSABUF       m_wsaBuffer;

int          m_RecvPos;

char         m_Buffer[1024*8];//此缓冲要尽可能大

OVERLAPPEDPLUS(IOType ioType) {

ZeroMemory(this, sizeof(OVERLAPPEDPLUS));

m_ioType = ioType;

}

};

接收连接后发出的第一个请求,请求接收包头大小的数据.

OVERLAPPEDPLUS *pOverlappedPlus = new OVERLAPPEDPLUS;

pOverlappedPlus->m_wsaBuffer.buf = pOverlappedPlus->m_Buffer;

pOverlappedPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;///包头的长度

pOverlappedPlus->m_bIsPackageHead = true;

pOverlappedPlus->m_RecvPos = 0;

pOverlappedPlus->m_ioType = IORead;

DWORD RecvBytes;

DWORD Flags;

Flags = 0;

if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,

&pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)

{

if (WSAGetLastError() != ERROR_IO_PENDING)

{

delete pOverlappedPlus;

}

else

{

///相关的错误处理

}

}

else

{

///相关的错误处理

}

在GetQueuedCompletionStatus所在的函数中.

if( pOverlapPlus->m_ioType== IORead)

{

if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )

{

if( pOverlapPlus->m_bIsPackageHead == true )///接收到的是包头。

{

PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer);

if(pThis->IsLegalityPackageHead(pPackageHead)==false)///判断是否是合法的包

{

closesocket(lpClientContext->m_Socket);

continue;

}

pOverlapPlus->m_bIsPackageHead = false;

pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;

pOverlapPlus->m_RecvPos += dwIoSize;

pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;

}

else///接收到的是包体

{

pOverlapPlus->m_RecvPos += dwIoSize;

///这时pOverlapPlus->m_Buffer里就存放了一个完整的数据包,长度为pOverlapPlus->m_RecvPos

///继续请求 请求下一个数据包的包头

pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer;

memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));

pOverlapPlus->m_wsaBuffer.len = PACKAGE_HEAD_LEN;

pOverlapPlus->m_bIsPackageHead = true;

pOverlapPlus->m_RecvPos = 0;

}

}

else///接收的数据还不完整

{

pOverlapPlus->m_wsaBuffer.len -= dwIoSize;

pOverlapPlus->m_RecvPos += dwIoSize;

pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;

}

pOverlapPlus->m_ioType = IORead;

state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,

&pOverlapPlus->m_ol, NULL);

if ( state == SOCKET_ERROR)

{

if(WSAGetLastError() != ERROR_IO_PENDING)

{

//关闭套接字 释放相应资源

continue;

}

}

}

三:如何判断包的合法性.

判断包的合法性可以结合下面两种方式来判断.但是想100%的判定出非法包,只能通过信息安全中的知识来判定了,对这种方法这里不做阐述.

1.通过包头的结构来判断包的合法性.

最初的时候我是根据包头来判断包的合法性,比如判断Command是否超出命令范围,nDataLen是否大于最大包的长度.但是这种方法无法过滤掉非法包,当出现非法包时我们唯一能做的就是断开连接,或许这也是最好的处理办法.

我们可以给一个完整的包加上开始和结束标志,标志可以是个整数,也可以是一串字符串.以第一种拆包方式为例来说明.当要拆一个完整包时我们先从缓冲区有效数据头指针地址搜索包的开始标志,搜索到后并且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据代表数据长度的变量的值定位到包尾,判断包尾标志是否与我们定义的一致,若一致则这个包是合法的包.若有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据全部舍弃.

2.通过逻辑层来判断包的合法性.

当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接

参考链接:https://blog.csdn.net/chenxiao88957478/article/details/50903364

c语言 udp 解包_Socket拆包和解包相关推荐

  1. c语言 udp 解包_UDP打包及解包问题

    UDP打包及解包问题 (2012-04-11 00:12:39) 标签: 打包 包 杂谈 UDP打包及解包问题第一个问题: 想问下在VC++开发平台下,是否提供了UDP协议打包和解包的接口函数?通常所 ...

  2. websocket底层处理粘包_Socket解决粘包问题1

    粘包是指发送端发送的包速度过快,到接收端那边多包并成一个包的现象,比如发送端连续10次发送1个字符'a',因为发送的速度很快,接收端可能一次就收到了10个字符'aaaaaaaaaa',这就是接收端的粘 ...

  3. python socket 心跳包_Socket之心跳包实现思路

    由于最近要做一个客户端,但是要求有一个掉线检测的功能,下面让我们看看使用自定义的HeartBeat方式来检测客户端的连接情况. 心跳包的实现思路: 客户端连接上服务端后,在服务端会维护一个在线客户端列 ...

  4. TCP利用封包和解包解决“粘包”问题

    本文参考自徐晓鑫<后台开发>,给出一个可实际应用的demo,该demo核心在于封包和解包的思想,以及自定义发送.接收数据. 一.TCP粘包现象 what? TCP是个"流&quo ...

  5. IO系列学习总结八:以Netty的聊天室程序为例,再聊聊拆包粘包的解决方案

    前言 在上篇文章:IO系列学习总结七:从官网Factorial协议的视角来理解出站.入站及数据碎片化(粘包拆包)中,针对Factorial协议做了一些解析,其中包含netty的入站出站处理顺序以及粘包 ...

  6. UDP和TCP 的半包、粘包、拆包

    ###简介 tcp是以流式发送数据的,数据之间无明显界限,只能人为的划分数据包的边界 在发送过程中 会因为缓冲区太小在传输超大包时 出现"半包" 会因为接收端消费速度太慢,导致数据 ...

  7. Netty 中的粘包和拆包详解

    Netty 底层是基于 TCP 协议来处理网络数据传输.我们知道 TCP 协议是面向字节流的协议,数据像流水一样在网络中传输那何来 "包" 的概念呢? TCP是四层协议不负责数据逻 ...

  8. rtp h264打包和解包

    1.学习思路 在了解rtp对h264数据的打包之前,需要对rtp.h264的一些基本概念有一个初步的了解,然后再使用rtp对h264的打包解包做解析 2.rtp认知 实时传送协议(Real-time ...

  9. Python赋值、打包和解包,90%人不清楚的知识点!

      人生苦短,快学Python! 关于赋值.打包和解包这 3个概念,我觉得有必要做一个分享,因为很多朋友确实不清楚. 如果有这样一个元组t: >> t = ('foo', 'bar', ' ...

最新文章

  1. c语言——求单词个数
  2. Android开发之程序猿必需要懂得Android的重要设计理念2(5.20更新版)
  3. 不懂AI的我,是如何搞开发的?
  4. 机器学习Sklearn实战——梯度提升树二分类原理
  5. 当前最主流的商业模式,普通老板如何打造强大后端实现躺赚的秘诀!
  6. SAP License:FI-CO集成
  7. pom中导入spingside包
  8. 动态磅是怎么原理_浅谈动态地磅的原理及未来发展方向
  9. Android 安卓益智休闲源码
  10. 安全防御——防病毒网关
  11. android 2d 漫画界面,开源漫画项目,部分界面仿有妖气
  12. 改善客户服务体验的 5 种方法
  13. 基于OpenCASCADE自制三维建模软件(十)编译ASSIMP库
  14. 如何提升客户的信任,实现与客户的连接,带动业绩的转变
  15. r语言worldclim数据_MAXENT数据准备:气候数据与高程数据格式的统一
  16. 券业零售业务转型方向已显现?从打造个人IP开始
  17. 传感器实训心得体会_生产线实习心得体会范文
  18. Windows 10离线安装dotnetfx35
  19. OneHotEncoder独热编码
  20. 微信小程序获取定位后使用腾讯地图解析到市位置

热门文章

  1. 从零学会Photoshop经典教程300集
  2. python定义一个student类、有下面的_Python的类和方法——成员可见性
  3. 微信小程序实现手写签名(极简横版)
  4. 三星手机上,setOnEditorActionListener软键盘监听回车无效,解决方案
  5. 正在载入中......loading页面的几种方法
  6. 图片base64编码的前端展示及后端解码,编码
  7. iOS 3DTouch的小细节
  8. cad审图软件lisp_CAD审图标记最新版
  9. OA系统面试时如何介绍的思路
  10. 一个父亲对新婚女儿的自白