SOCKET 封包和拆包

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

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

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及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的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷.再后来就是对数据包进行封包和拆包的操作.
    封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.
    对于拆包目前我最常用的是以下两种方式.
    1.动态缓冲区暂存方式.之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.
    大概过程描述如下:
    A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.
    B,当接收到数据时首先把此段数据存放在缓冲区中.
    C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.
    D,根据包头数据解析出里面代表包体长度的变量.
    E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.
    F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

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

下面给出相关代码.

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.
用代码来说明.注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改.

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:
OVERLAPPEDm_ol;
IOTypem_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;
}
}

}

时间: 2024-10-06 14:52:39

SOCKET 封包和拆包的相关文章

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

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

网络通讯的封包和拆包

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

TCP的封包与拆包

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

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++实现对windwos 下socket 的封装(实现封包及拆包处理)

SuperSocket.h #pragma once #include<string> #include<iostream> #include <WINSOCK2.H> #include "MyThread.h" #include "SuperThread.h" using namespace std; class SuperSocket { public: typedef struct SockData{ int id; int

C# Socket系列三 socket通信的封包和拆包

通过系列二 我们已经实现了socket的简单通信 接下来我们测试一下,在时间应用的场景下,我们会快速且大量的传输数据的情况! 1 class Program 2 { 3 static void Main(string[] args) 4 { 5 TCPListener tcp = new TCPListener(); 6 TSocketClient client = new TSocketClient(); 7 for (int i = 0; i < 10; i++) 8 { 9 client.

[Go] 轻量服务器框架tcp的粘包问题 封包与拆包

tcp传输的数据是以流的形式传输的,因此就没有办法判断到哪里结束算是自己的一个消息,这样就会出现粘包问题,多个包粘在一起了 可以使用这样一个自定义的形式来解决,一个消息分为 head+body  head包括数据的长度和数据编号 , 长度和编号都是uint32类型 也就是32位 占有4个字节 , 总共head占有8个字节 封装一个消息的结构体,作为一个数据实体,比如下面这个,编号 数据 数据长度  三个属性 package znet type Message struct { Id uint32

Socket开发

Socket开发框架之消息的回调处理 伍华聪 2016-03-31 20:16 阅读:152 评论:0 Socket开发框架之数据加密及完整性检查 伍华聪 2016-03-29 22:39 阅读:709 评论:2 Socket开发框架之数据传输协议 伍华聪 2016-03-26 00:00 阅读:1049 评论:1 Socket开发框架之框架设计及分析 伍华聪 2016-03-22 11:15 阅读:1931 评论:6 Socket开发框架之数据采集客户端 伍华聪 2011-06-08 23:4

Java socket编程API基础

Socket是Java网络编程的基础,深入学习socket对于了解tcp/ip网络通信协议很有帮助, 此文讲解Socket的基础编程.Socket用在哪里:①.主要用在进程间,②.网络间通信. 文章目录如下: 一.Socket通信基本示例 二.消息通信优化 2.1 双向通信,发送消息并接受消息 2.2 使用场景 2.3 如何告知对方已发送完命令 2.3.1 通过Socket关闭 2.3.2 通过Socket关闭输出流的方式 2.3.3 通过约定符号 2.3.4 通过指定长度 三.服务端优化 3.