本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力
这个问题也可能这么问:网络通信时,如何解决粘包、丢包或者包乱序问题?
首先,我们要明确 TCP 协议的特点:
- 面向连接
- 可靠的
- 基于字节流
- 全双工
其中可靠就是保证包之前的序号,尽可能保证不丢包。所以在一般情况,是不存在丢包和包乱序问题的。TCP 通信是可靠通信方式,TCP 协议栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发到目的地。
所以一般出现的问题就在:粘包问题。
概念解释
什么是粘包?
粘包就是连续给对端发送两个或者两个以上的数据包,对端在一次收取中可能收到的数据包大于 1 个,大于 1 个,可能是几个(包括一个)包加上某个包的部分,或者干脆就是几个完整的包在一起。
所以半包的概念也出来了。如果收到的数据只是一个包的部分,这就出现半包。
无论是粘包还是半包,本质是因为 TCP 协议是流式协议。所以解决方案就是如何是 stream 中区分出包,更准确来说:就是如何在一个无边界的数据流中确定一个固定大小的数据边界?
解决方案
主要有 3 种方法:
固定包长数据包
顾名思义:每个包长度都是固定的。举个例子,规定每个协议包的大小是 64 个字节,每次收满 64 个字节,就取出来解析;如果没有满,则暂存在 mem 中,TCP 流继续往后解析。
这种协议实现格式很简单,但是空间利用率可能会低。因为包长度不满足固定字长,剩余的空间就需要用特殊字符填充 (特殊字符需要和真实数据区分开)。
而包内容超过指定字节数,又得分包分片,需要增加额外处理逻辑:
- 发送端需要根据固定长包截断
- 接收端需要组装分片包
指定结束标志数据包
这种解决方案在协议处理上比较常见,即:字节流中遇到特殊的符号值时就认为到一个包的末尾。
例如,我们熟悉的 FTP协议,发邮件的 SMTP 协议,一个命令或者一段数据后面加上"\r\n"(即所谓的 CRLF)表示一个包的结束。对端收到后,解析数据流时,解析到 "\r\n" 就把之前解析组装一个数据包,交给后续的 处理函数 处理。
但是这里会出现一个问题:如果协议数据包内容部分出现了包结束标志字符,就需要对这些字符做转码或者转义操作,以免被接收方错误地当成包结束标志而误解析。
参照 Redis 的 RESP 协议: 如果是 多行字符串,就会在字符串数据包 + 前缀长度,以辅助区分数据包内容
header+content
header ⇒ 直接指定内容长度
content ⇒ 存储实际的数据包内容
type struct msg_header {
int32 bodySize;
int32 bodyContent;
};
首先,header & content 都是 8字节长度,所以在流读取的时候读取到 8 字节长度 → 解析 header → 解析 header 中包含的数据包长度,然后开始缓存数据包内容。
总结
一般的网络库不提供这些功能是出于需要支持不同的协议,由于协议的不确定性,因此没法预先提供具体解包代码。当然,这不是绝对的,也有一些网络库提供了这种功能:比如 netty
- DelimiterBasedFrameDecoder ⇒ 按特殊字符作为结束符的协议包
- ByteToMessageDecoder ⇒ 处理包头 + 包体 这种格式的数据包
- 开发者继承 ByteToMessageDecoder ⇒ 开发自己的定制协议
协议设计
这里采取 header + content 设计
先设计 header ⇒
// 协议头
type struct msg {
// 整个包体大小
int32 bodysize;
};
如何解包:
while (true) {
if (pBuffer.ReadBytes() < cap(msg)) {
// 不够一个包头大小
return;
}
// 取包头信息
msg header;
memcpy(&header, pBuffer.Peek(), cap(msg));
// 包头有错误,立即关闭连接
if (header.bodysize <= 0 || header.bodysize > MAX_PACKAGE_SIZE) {
// 客户端发非法数据包,服务器主动关闭
conn.Close();
return;
}
// 收到的数据不够一个完整的包
if (pBuffer.ReadBytes() < header.bodysize + cap(msg))
return;
pBuffer.Retrieve(cap(msg));
// inbuf用来存放当前要处理的包
string inbuf;
inbuf.append(pBuffer.Peek(), header.bodysize);
pBuffer.Retrieve(header.bodysize);
// 解包和业务处理
if (!Process(conn, inbuf.c_str(), len(inbuf))) {
// 客户端发非法数据包,服务器主动关闭
conn.Close();
return;
}
}