TCP 没有业务消息边界。“粘包”只是接收方一次读到多条消息,“拆包”只是一次只读到部分消息,它们都不是 TCP 故障。 正确做法是让协议明确边界,并让解码器支持任意分段。
Table of contents
Open Table of contents
四种常见边界方案
| 方案 | 优点 | 风险 |
|---|---|---|
| 固定长度 | 解析简单 | 浪费空间,扩展困难 |
| 分隔符 | 文本直观 | 转义、超长未闭合消息 |
| 长度字段 | 二进制高效 | 长度校验和字节序必须一致 |
| 协议自描述 | 表达力强 | 解析器更复杂 |
长度字段协议可以定义为:魔数、版本、消息类型、请求 ID、正文长度、正文。魔数是固定开头,用于快速拒绝错误协议;请求 ID 用于关联异步响应。
运行长度字段实验
Netty 长度字段样例下载先用四字节整数写入正文长度, 再把 UTF-8 正文写到网络缓冲区;解码器只有收到完整帧才向后传播:
mvn -Dtest=NetworkServerLabTest#framesAndDecodesOneNettyMessage test
真实协议必须限制最大帧长,并在分配正文缓冲区前验证长度非负、不过限。否则攻击者只发一个巨大长度字段,就可能制造内存压力。
编码和序列化不是同一个概念
编码把传输字节转换成消息边界或基础类型;序列化把业务对象转换成可传输表示,例如 JSON、Protobuf。两者常被放在同一 Pipeline,但职责不同。 协议还要定义版本兼容:新增可选字段、未知字段处理、枚举扩展和废弃周期。
不要使用 Java 原生对象序列化作为跨服务协议。它与 Java 类型强耦合,演进困难,并扩大反序列化安全风险。
背压保护内存和下游
背压是下游处理不过来时,主动减慢或拒绝上游,而不是无界缓存。Netty 可根据 Channel 可写状态和写缓冲区高低水位暂停生产;读取侧可以临时关闭自动读取, 处理完积压后再继续读。
完整策略包括:
- 每连接和全局的最大在途消息数;
- 最大帧、最大聚合正文和最大写队列;
- 排队超时与明确的过载响应;
- 对调用方可见的重试建议;
- 队列长度、不可写持续时间和拒绝数指标。
只调大缓冲区会把故障延迟到内存耗尽。限流决定“允许多少请求进入”,背压决定“下游变慢时怎样反馈”,超时决定“最多等待多久”,三者要一起设计。
下一步
阅读Netty 线程模型与任务调度,避免业务阻塞把整个 EventLoop 拖慢。