解决粘包、半包¶
概念¶
粘包:所谓粘包就是连续给对端发送两个或者两个以上的数据包,对端接收可能一次收到大于1个数据包
半包:半包也就是对端可能收到的数据包是半个
解决方法¶
固定包长的数据包¶
即每个协议包的长度是固定的
缺点:灵活性差。如果包内容不足指定的字节数,剩余的空间就要填充特殊的信息(如“\0”),因为如果不填充特殊的信息,如何区分数据包里的正常内容和其余内容?所以需要增加额外的处理逻辑
以指定字符(串)为包的结束标志¶
即字节流中遇到特殊的符号就认为是到了一个包的末尾了
缺点:如果协议数据包内容部分需要使用包结束符,就需要对这些字符进行转义等操作,以免被错误的当成是包结束符
包 + 包体格式¶
数据包 = 包头 + 包体
包头示例:
包体长度是固定的,如果对端先收取包头大小字节数(如果还不足则将其缓存起来),之后解析包头,包头中的bodySize
会指定包体的长度,等待包体收取完毕(同样的,不足一个包体则将其缓存起来)
解包和处理(ep)¶
包头格式
处理流程如下:
#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循环里,这样才可以处理多个数据包,否则一次只能处理一个