第 6 章 拆包、粘包与协议边界
6.1 不要猜一次 read 的长度
“粘包”通常指一次读取包含多条消息,“半包”指一次读取只有消息的一部分。它们是字节流读取的正常现象。不能靠延时、一次发送后睡眠、关闭 Nagle 算法来建立可靠的业务边界。
常见方案如下:
| 方案 | 如何找边界 | 适用例子 | 主要限制 |
|---|---|---|---|
| 固定长度 | 每 N 字节一条 | 简单设备协议 | 变长内容浪费空间 |
| 分隔符 | 找到约定结束符 | 文本命令 | 正文须转义或禁止分隔符 |
| 长度字段 | 先读头部,再读指定长度 | 二进制协议 | 长度验证必须严格 |
| 更高层协议 | 使用已有标准编解码器 | HTTP、WebSocket | 须遵守标准的完整语义 |
6.2 为什么先切帧再解码中文
UTF-8 中一个中文字符常占 3 个字节,但 TCP 可能在任意字节位置分开。如果把每次收到的 ByteBuf 独立转成字符串,可能把一个字符的前半截当成错误序列。
本书先由行解码器积累完整帧,再由字符串解码器一次转换。测试专门把“你”的第一个字节与其余字节拆开送入,验证结果仍为“你”。这说明测试要模拟网络允许的行为,不能只验证最理想的整包输入。
6.3 长度字段协议的例子
假设一条帧由 4 字节长度和 N 字节正文组成。长度字段使用大端序,表示正文长度,不包含自己的 4 字节。
00 00 00 03 | 61 62 63
正文长度 3 | a b c
可以使用:
new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4)五个参数分别表示:最大帧总长度 1024;长度字段偏移量 0;字段宽度 4;额外调整值 0;解码后跳过前 4 字节。因为长度值已经表示正文长度,解码器会结合长度字段末端位置算出整帧长度。这里允许的正文最大值是 1020,而不是 1024。[3]
发送方向常搭配 LengthFieldPrepender(4),它默认将正文长度作为 4 字节头加到前面。若业务发字符串,Pipeline 可按以下加入顺序组装:
LengthFieldBasedFrameDecoder
StringDecoder
LengthFieldPrepender
StringEncoder
业务处理器
从业务处理器通过 ctx.writeAndFlush 发出字符串后,出站先把字符串变为字节,再在字节前添加长度。不要再添加换行符,除非新协议明确要求它。
6.4 限制长度是协议的一部分
如果允许客户端发送无限长、永远没有结束符的内容,服务端可能持续积累内存。必须限定帧长,并定义超限后的行为。示例的异常处理器会记录异常并关闭连接。
行解码器的默认超限报告时机与显式启用 failFast 时不同。测试采用 new LineBasedFrameDecoder(8, true, true),明确要求发现超限时尽早报错,避免把实现细节当成普遍保证。
小练习: 长度字段值为 12、头部为 4 字节时,线上整帧有多少字节?若最大帧长为 1024,正文最多多长?