网络通讯的封包和拆包

封包和拆包(经典收藏)
对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.自从我从事网络通讯编程工作以来(大概有三年的时间了),我一直在思索和改进封包和拆包的方法.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了.
一.为什么基于TCP的通讯程序需要进行封包和拆包.
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data2余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data1的部分数据,然后接收到了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.通过逻辑层来判断包的合法性.
当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接.

时间: 2024-12-24 22:35:41

网络通讯的封包和拆包的相关文章

SOCKET 封包和拆包

对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.自从我从事网络通讯编程工作以来(大概有三年的时间了),我一直在思索和改进封包和拆包的方法.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了. 一.为什么基于TCP的通讯程序需要进行封包和拆包. TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的

TCP的封包与拆包

对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包. 一.为什么基于TCP的通讯程序需要进行封包和拆包. TCP是个"流"协议,所谓流,就是没有界限的一串数据. 大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包. 由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况: 假设我们连续调用两次 send 分别发送两段数据 data1

C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解决Socket粘包.半包问题. 更甚至,某些师德有问题的老师,根本就没跟你说过Socket的粘包.半包问题是什么玩意儿. 直到有一天,你的Socket程序在传输信息时出现了你预期之外的结果(多于的信息.不完整的信息.乱码.Bug等等). 任你喊了一万遍“我擦”,依旧是不知道问题出在哪儿! 好了,不说

如何在Windows系统上用抓包软件Wireshark截获iPhone等网络通讯数据

http://www.jb51.net/os/windows/189090.html 今天给大家介绍一种如何在Windows操作系统上使用著名的抓包工具软件Wireshark来截获iPhone.iPad等iOS设备或Android设备的网络通讯数据的方法 不管是iPhone的iOS还是Android系统上开发的应用App基本上都会需要使用网络通讯来传输数据.开发者有的时候可能会需要通过抓包的方式来了解应用具体在传输些什么数据,以及数据是否正确.今天给大家介绍一种如何在Windows操作系统上使用

Android网络通讯简介

网络通信应该包含三部分的内容:发送方.接收方.协议栈.发送方和接收方是参与通信的主体,协议栈是发送方和接收方进行通信的契约.按照服务类型,网络通信可分为面向连接和无连接的方式.面向连接是在通信前建立通信链路,而通信结束后释放该链路.无连接的方式则不需要在通信前建立通信连接,这种方式不保证传输的质量. Android提供了多种网络通信的方式,如Java中提供的网络编程,在Android中都提供了支持.Android中常用的网络编程方式如下: 针对TCP/IP协议的Socket和ServerSock

rtp的封包与拆包h264

请看文档rfc3984 1.看h264的帧 SPS序列参数帧 00 00 00 01 67 64 .... PPS图像参数帧 00 00 00 01 68 EE.... I帧 00 00 00 01 65 EE.... P帧 00 00 00 01 61 E0 ... 2. rtp头 RTP 头格式如下: RTP 头的结构: 0                   1                   2                   3 0 1 2 3 4 5 6 7 8 9 0 1

[转] C#.Net Socket网络通讯编程总结

1.理解socket1).Socket接口是TCP/IP网络的应用程序接口(API).Socket接口定义了许多函数和例程,程序员可以用它们来开发TCP/IP网络应用程序.Socket可以看成是网络通信上的一个端点,也就是说,网络通信包括两台主机或两个进程,通过网络传递它们之间的数据.为了进行网络通信,程序在网络对话的每一端都需要一个Socket. 2).TCP/IP传输层使用协议端口将数据传送给一台主机的特定应用程序,从网络的观点看,协议端口是一个应用程序的进程地址.当传输层模块的网络软件模块

《连载 | 物联网框架ServerSuperIO教程》-4.如开发一套设备驱动,同时支持串口和网络通讯。附:将来支持Windows 10 IOT

感谢唯笑志在分享 原博主原地址:http://www.cnblogs.com/lsjwq/ 注:ServerSuperIO有可能被移植到Windows 10 IOT上,那么将来有可能开发一套设备驱动,可以支行在服务端.嵌入式设备中,将形成完整的解决方案.       现在已经调试通过部分代码,还得需要一段时间,一般都是晚上干,时间也有限.如下图: 目       录 4.如开发一套设备驱动,同时支持串口和网络通讯... 2 4.1           概述... 2 4.2          

网络--三种网络通讯方式及Android的网络通讯机制

Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口).Org.apache接口和Android.net.*(Android网络接口).下面分别介绍这些接口的功能和作用. 1.标准Java接口 java.net.*提供与联网有关的类,包括流.数据包套接字(socket).Internet协议.常见Http处理等.比如:创建URL,以及URLConnection/HttpURLConnection对象.设置链接参数.链接到服务器.向服务器写数据.从服务器读取