1、如何解决粘包问题?
在设计网络协议时,可能会存在粘包、丢包或者包乱序问题,但TCP协议时可靠性协议,大多数情况不存在丢包和乱序问题,但UDP协议如果不能接受少量丢包,就必须自己设计有序和可靠性传输机制(比如:RTP协议、RUDP协议,)。因此,只存在如何解决粘包的问题。
TCP 协议是流式数据格式。解决问题的思路还是想办法从收到的数据中把包与包的边界给区分出来。那么如何区分呢?目前主要有三种方法:
- 固定包长的数据包
- 以指定字符(串)为包的结束标志
- 包头 + 包体格式
2、如何处理解包问题?
包头 + 包体 这种格式的数据包的捷豹处理的流程如下:
假如 包头格式如下:
//强制一字节对齐
#pragma pack(push, 1)
//协议头
struct msg_header
{
int32_t bodysize; //包体大小
};
#pragma pack(pop)
代码流程如下:
//包最大字节数限制为10M
#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_header))
{
//LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg_header)=" << sizeof(msg_header);
return;
}
//取包头信息
msg_header header;
memcpy(&header, pBuffer->peek(), sizeof(msg_header));
//包头有错误,立即关闭连接
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_header))
return;
pBuffer->retrieve(sizeof(msg_header));
//inbuf用来存放当前要处理的包
std::string inbuf;
inbuf.append(pBuffer->peek(), header.bodysize);
pBuffer->retrieve(header.bodysize);
//解包和业务处理
if (!Process(conn, inbuf.c_str(), inbuf.length()))
{
//客户端发非法数据包,服务器主动关闭之
LOGE("Process package error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str());
conn->forceClose();
return;
}
}// end while-loop
}
pBuffer 这里是一个自定义的接收缓冲区,这里的代码,已经将收到的数据放入了这个缓冲区,所以判断当前已收取的字节数目只需要使用这个对象的相应方法即可,需要注意如下问题:
- 取包头之前:需判断缓冲区 pBuffer 中的可读数据大小是否超过一个报文长度,否则直接退出。
- 取包头之后:需判断报文体大小是否超过自己规定最大值,避免发超大型数据耗尽内存。
- 多帧处理:使用while一直处理报文,直到缓冲区数据不足一帧报文结束。