bigywater.tech
Java · 149

Netty 粘包拆包:TCP 为什么没有消息边界

粘包、拆包到底是不是真的?这篇先用纯 JDK 的 socket 量出 TCP 的字节流行为,再用 Netty 4.1 的三种帧解码器恢复消息边界(Selector 的工作方式见上一篇)。

855 次 read
2000 条 50 字节消息被合并后接收方 read 的次数
34 次
每条 20000 字节时读到不整条的 read 次数(拆包)
50 → 2000
不加 / 加长度解码器时 2000 条消息触发的 channelRead 次数

TCP 字节流

实测 · 本机执行输出
  发送方写了 2000 次、每次 50 字节(共 100000 字节);接收方 read() 了 855 次,共 100000 字节;平均每次读到 116 字节
  每次 read 读到的字节数不是 50 的整数倍的次数:0 / 855(消息被拦腰截断)
  换成每条 20000 字节、写 100 条(共 2,000,000 字节),接收方用 200000 字节的缓冲区:read() 58 次,其中读到的字节数不是 20000 的整数倍的有 34 次(这些读取里有被截断的消息)
  发送方一次 write 1,000,000 字节,接收方用 1 MB 的缓冲区读,read() 了 13 次才收完 1000000 字节

没有解码器的 Netty

实测 · 本机执行输出
  发送方写了 2000 条消息;服务端 channelRead 被调用 50 次,收到 100000 字节,每次的大小最小 150、最大 32768

长度字段解码

实测 · 本机执行输出
  发送方写了 2000 条消息;服务端 channelRead 被调用 2000 次,收到 92000 字节,每次的大小最小 46、最大 46;解码出 2000 条完整消息,内容校验失败 0 条

分隔符与固定长度

实测 · 本机执行输出
  发送方写了 2000 条消息;服务端 channelRead 被调用 2000 次,收到 96000 字节,每次的大小最小 48、最大 48;解码出 2000 条完整消息,内容校验失败 0 条
  [Fixed]   发送方写了 2000 条消息;服务端 channelRead 被调用 2000 次,收到 100000 字节,每次的大小最小 50、最大 50;解码出 2000 条完整消息

分步演示

TCP 帧
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,Netty 4.1.115.Final(netty-all),回环地址上的 TCP
    • 没有验证:真实网络(有 MTU、丢包、延迟)下的粘包拆包比例、Nagle 算法与 TCP_NODELAY 的影响(只设了 TCP_NODELAY 一种)、半包超过 maxFrameLength 的异常处理、其它解码器(DelimiterBasedFrameDecoder、HTTP 解码)、ByteToMessageDecoder 的累积缓冲细节;read 次数是一次运行的结果,每次不同
    京ICP备2026067502号-1京公网安备11010502063487号