打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
[z]基于TCP的通讯程序 进行封包和拆包一
对于基于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;
}
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
关于Tcp封包
无符号哥伦布编码的解码原理以及C 实现
java基于TCP的socket数据包拆分方法
java socket/Serversocket编程详解(中/英文)
TCP之深入浅出send和recv
socket的半包,粘包与分包的问题
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服