跳转至

解决粘包、半包

概念

粘包:所谓粘包就是连续给对端发送两个或者两个以上的数据包,对端接收可能一次收到大于1个数据包

半包:半包也就是对端可能收到的数据包是半个

解决方法

固定包长的数据包

即每个协议包的长度是固定的

缺点:灵活性差。如果包内容不足指定的字节数,剩余的空间就要填充特殊的信息(如“\0”),因为如果不填充特殊的信息,如何区分数据包里的正常内容和其余内容?所以需要增加额外的处理逻辑

以指定字符(串)为包的结束标志

即字节流中遇到特殊的符号就认为是到了一个包的末尾了

缺点:如果协议数据包内容部分需要使用包结束符,就需要对这些字符进行转义等操作,以免被错误的当成是包结束符

包 + 包体格式

数据包 = 包头 + 包体

包头示例

struct msg_header
{
    int32_t bodySize; //指定包体长度
    int32_t cmd;
};

包体长度是固定的,如果对端先收取包头大小字节数(如果还不足则将其缓存起来),之后解析包头,包头中的bodySize会指定包体的长度,等待包体收取完毕(同样的,不足一个包体则将其缓存起来)

解包和处理(ep)

包头格式

#pragma pack(push, 1) //以一字节对齐

struct msg
{
    int32_t bodySize; //包体大小
};
#pragma pack(pop)

处理流程如下:

#define MAX_PACKAGE_SIZE 10*1024*1024

void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, TimeStamp receivTime)
{
   while(true) {
       //不够一个包的大小
       if(pBuffer->readableBytes() < (size_t)sizeof(msg)) {
           return;
       }

       //取包头信息
       msg header;
       memcpy(&header, pBuffer->peek(), sizeof(msg));

       //包头有错误,立即关闭
       if(header.bodySize <= 0 || header.bodySize > MAX_PACKAGE_SIZE) {
           //客户端发送的是非法的数据,服务端主动关闭连接
           LOGE("Illegal package, bodysize: %lld, close TcpConnection, client: %s", header.bodySize, conn->peerAddress().toIpPort().c_str());
           conn->forceClose();
           return;
       }

       //收到的数据不足一个完整的包
       if(pBuffer->readableBytes() < (size_t)header.bodySize + sizeof(msg))
           return;

       pBuffer->retrieve(sizeof(msg));
       //inbuf用来存储当前要处理的数据包
       std::string inbuf;
       inbuf.append(pBuffer->peek(), header.bodySize);
       pBuffer->retrieve(header.bodySize);

       //解包和业务处理
       if(!Process(conn, inbuf.c_str(), inbuf.length())) {
           //客户端发送的是非法的数据,服务端主动关闭连接
           LOGE("Illegal package, bodysize: %lld, close TcpConnection, client: %s", header.bodySize, conn->peerAddress().toIpPort().c_str());
           conn->forceClose();
           return;
       }
   } // end while-loop
}

注意:

  • 取包头时,应该使用拷贝的方式将包头数据备份出来,而不是直接从缓冲区中取出来。原因:接下来会根据包头中得到包体的大小,如果剩余数据不足一个包体的大小,又得把这个包放回缓冲区
  • 通过包头得到包体的大小,要对bodySize的大小进行判断,即if(header.bodySize <= 0 || header.bodySize > MAX_PACKAGE_SIZE)。原因:我们会缓存对端发送过来的数据,假设不对包体的大小进行协议上的数值判断,则如果有非法的数据包发送过来,服务器这边会缓存过大的数据,如果数据量庞大就会导致服务器崩溃
  • 需要将整个包头和包体的处理过程放入一个while循环里,这样才可以处理多个数据包,否则一次只能处理一个

图解(ep)

解决粘包和半包




参考“张小方”的高性能服务器公众号