Netty 如何解决 TCP 粘包、拆包的问题?

#中间件#面试八股

Netty 如何解决 TCP 粘包、拆包的问题?

结论先行:TCP 是字节流,本身没有消息边界,粘包与拆包是必然现象而不是错误。Netty 的思路是“在字节流上重建边界”,提供固定长度、分隔符、长度字段三类解码器,工程上最常用的是长度字段方案 LengthFieldBasedFrameDecoder。

一、为什么会粘包拆包

发送方 write("hello") + write("world")

接收方可能读到:
  "helloworld"        ← 粘包(两次写被合并)
  "hello" "world"     ← 正常
  "hel" "lowor" "ld"  ← 拆包(受 MSS、缓冲区、Nagle 影响)

原因:TCP 只保证字节有序到达,不保证“一次读对应一次写”
原因说明
发送缓冲合并Nagle 算法把小块数据攒起来一起发
MSS 分片超过最大报文段长度被 IP 层切开
接收缓冲应用读取速度与到达速度不匹配
一次读多次写接收缓冲区里可能已有多条完整消息

二、三类解码器

方案解码器优点缺点
固定长度FixedLengthFrameDecoder实现最简单不足要补齐,浪费带宽
分隔符DelimiterBasedFrameDecoder可读性好,适合文本协议内容里不能出现分隔符,需转义
长度字段LengthFieldBasedFrameDecoder二进制协议通用,效率高需要协议头约定长度位置

三、长度字段方案的五个参数

new LengthFieldBasedFrameDecoder(
        maxFrameLength,      // 最大帧长,防御超大包
        lengthFieldOffset,   // 长度字段的起始偏移
        lengthFieldLength,   // 长度字段占几个字节
        lengthAdjustment,    // 长度值是否需要修正
        initialBytesToStrip // 解码后丢弃多少字节(通常是头部)
);
协议格式:| length(4B) | type(1B) | payload(N B) |
            offset=0     len=4       实际帧长 = 4 + 1 + N

参数:maxFrameLength=65535, lengthFieldOffset=0, lengthFieldLength=4,
      lengthAdjustment=1, initialBytesToStrip=5

Netty 的帧长公式:
  frameLength = lengthFieldValue + lengthAdjustment + (offset + lengthFieldLength)
  本例:N + 1 + (0 + 4) = N + 5,正好覆盖 length + type + payload 三部分

常用组合对照:

length 字段的含义lengthAdjustmentinitialBytesToStrip
length 之后到帧尾的字节数(最常见)00(保留头部)或 4(去掉长度字段)
length 含自身,即整帧长度-4(负的长度字段长度)0
length 只含业务 payload,中间隔 1 字节 type15

四、服务端装配

ch.pipeline()
  .addLast(new LengthFieldBasedFrameDecoder(65535, 0, 4, 0, 4))
  .addLast(new LengthFieldPrepender(4))          // 编码时自动补长度
  .addLast(new ByteToMessageDecoder() {          // 帧内再解析业务字段
      @Override
      protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
          if (in.readableBytes() < 1) return;
          byte type = in.readByte();
          out.add(new Frame(type, in.readRetainedSlice(in.readableBytes())));
      }
  })
  .addLast(new BusinessHandler());

LengthFieldPrepender 与 LengthFieldBasedFrameDecoder 是配套的:一个写头,一个读头,双方约定同一套参数。

常见追问

  • 追问:为什么 UDP 不用考虑粘包?UDP 是数据报协议,保留消息边界,一次 receive 对应一个数据报(但可能丢包、乱序)。
  • 追问:maxFrameLength 设大了有什么风险?攻击者可以声明 1GB 长度让服务端预分配内存,所以要设上限并对超限连接直接关闭。
  • 追问:分隔符方案怎么处理内容里出现分隔符?做转义(如 \n 转 \\n),或用长度字段方案,二进制协议基本都选后者。

小结

  • 粘包拆包源于 TCP 无边界,只能由应用层重建边界。
  • 三类方案:定长、分隔符、长度字段;二进制协议首选长度字段。
  • lengthAdjustment 与 initialBytesToStrip 是最容易配错的两个参数。
  • 编解码器必须成对出现,参数要在协议文档里写死。
笔记加载中…