关闭

tcp封包才,解包

631人阅读 评论(0) 收藏 举报
分类:
origin: http://www.cnblogs.com/lvcha/articles/1875250.html
对于基于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.通过逻辑层来判断包的合法性. 当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接. 文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/3_program/cshapo/csharpjs/20100714/441891.html
0
0
查看评论

串口中怎样接收一个完整数据包的解析

这里以串口作为传输媒介,介绍下怎样来发送接收一个完整的数据包。过程涉及到封包与解包。设计一个良好的包传输机制很有利于数据传输的稳定性以及正确性。串口只是一种传输媒介,这种包机制同时也可以用于SPI,I2C的总线下的数据传输。在单片机通信系统(多机通信以及PC与单片机通信)中,是很常见的问题。 一、...
  • lpp0900320123
  • lpp0900320123
  • 2014-06-03 11:07
  • 53763

TCP/IP 网络数据封包和解包

TCP/IP 网络数据以流的方式传输,数据流是由包组成,如何判定接收方收到的包是否是一个完整的包就要在发送时对包进行处理,这就是封包技术,将包处理成包头,包体 包头是包的开始标记,整个包的大小就是包的结束标记。接收方只要按同样的方式解包即可,下面是我在网上搜罗的一个网络服务端和客户端程序代码。 ...
  • dai_jing
  • dai_jing
  • 2014-01-06 11:48
  • 11273

SOCKET 封包和拆包

对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.自从我从事网络通讯编程工作以来(大概有三年的时间了),我一直在思索和改进封包和拆包的方法.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了. 一.为什么基于TCP的通讯程序需...
  • e_wsq
  • e_wsq
  • 2013-10-17 12:14
  • 2290

最简单的TCP网络封包解包(补充)-序列化

出处:http://www.cppblog.com/tx7do/archive/2011/05/07/145865.html 最简单的TCP网络封包解包(补充)-序列化 如若描述或者代码当中有谬误之处,还望指正。 将数据能够在TCP中进行传输的两种方法 1.直接拷贝struct就...
  • wqvbjhc
  • wqvbjhc
  • 2011-12-31 17:27
  • 4051

socket 封包和解包

对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.下面就针对这个问题谈谈我的想法,抛砖引玉.若有不对,不妥之处,恳求大家指正.在此先谢过大家了.   一.为什么基于TCP的通讯程序需要进行封包和拆包. TCP是个"流"协议,所谓流,就是没有界...
  • dai_jing
  • dai_jing
  • 2014-04-18 19:48
  • 13633

游戏汉化技术内幕 第2章解包封包

作者: 坑神姐姐 AD http://kdays.cn/第1章 KRKR游戏系统汉化说明封包(Pack) —— 相对的就是解包(Unpack)实际上能解包的 不代表就能封包因为解包仅仅代表是从文件中提取出数据 但不代表能更改数据最典型极端的例子就是MTS的gift。里面...
  • luozhuang
  • luozhuang
  • 2008-12-19 18:28
  • 6243

scoket通讯过程中的封包,解包与异或加密等

原文链接:http://blog.csdn.net/qq992817263/article/details/50164931 工程链接:http://download.csdn.net/detail/jj391/9851444 该工程是根据原文修改而来。
  • jj391
  • jj391
  • 2017-05-25 22:03
  • 383

TCP通信原理及封包问题(详细,案例解析)

TCP大致工作原理介绍:   工作原理 TCP-IP详解卷1第17章中17.2节对TCP服务原理作了一个简明介绍(以下蓝色字体摘自《TCP-IP详解卷1第17章17.2节》): 尽管T C P和U D P都使用相同的网络层(I P),T C P却向应用层提供与UD P完全不同的服务。 ...
  • tiandijun
  • tiandijun
  • 2014-12-16 21:54
  • 2253

iOS/Android JSON封包与解包

一、iOS JSON封包与解包 iOS自带的JSON解决方案(NSJSONSerialization),它提供了针对NSDictionary、NSArray对象进行JSON数据的封包和解包。 1、JSON封包: 针对NSDictionary: NSDict...
  • CDUT100
  • CDUT100
  • 2016-07-21 19:55
  • 866

Python&Flash开发斗兽棋---2.1网络数据打包与解包

既然选择了原生态的SOCKET,那么首先要面对的问题就是数据的打包/解包问题。 对于服务器端,我们使用struct作为数据打包/解包工具,并对struct作了简单了解(相关资料:Python模块学习 ---- struct 数据格式转换)。 在Flash客户端,我们数据处理使用的是ByteArr...
  • ctbinzi
  • ctbinzi
  • 2012-04-30 23:30
  • 1863
    个人资料
    • 访问:1387002次
    • 积分:24383
    • 等级:
    • 排名:第323名
    • 原创:733篇
    • 转载:1240篇
    • 译文:2篇
    • 评论:73条
    最新评论