【Netty系列】解决TCP粘包和拆包:LengthFieldBasedFrameDecoder
目录
如何使用?
1. 示例代码(基于Netty)
2. 关键参数解释
3. 协议格式示例
4. 常见配置场景
场景1:长度字段包含自身
场景2:长度字段在消息中间
5. 注意事项
举个例子
完整示例:客户端与服务端交互流程
1. 服务端代码(含响应)
2. 客户端代码(含编码器)
3. 执行流程说明
4. 网络包结构示意图
5. 关键点总结
如何使用?
以下是使用 LengthFieldBasedFrameDecoder
解决 TCP 粘包/拆包问题的 完整代码示例 和 关键解释:
1. 示例代码(基于Netty)
// Server端代码示例
public class NettyServer {public static void main(String[] args) {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {// 关键:添加 LengthFieldBasedFrameDecoderch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, // maxFrameLength(最大帧长度)0, // lengthFieldOffset(长度字段偏移量)4, // lengthFieldLength(长度字段占4字节)0, // lengthAdjustment(长度调整值)4 // initialBytesToStrip(跳过前4字节,因为长度字段已解析)));// 将ByteBuf转为String(按需替换为实际解码器)ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));// 自定义业务处理器ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("Received message: " + msg);}});}});ChannelFuture future = bootstrap.bind(8080).sync();future.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
2. 关键参数解释
LengthFieldBasedFrameDecoder
的构造函数参数如下:
参数 | 说明 |
| 允许的最大帧长度(防止内存溢出) |
| 长度字段的起始偏移量(通常为0) |
| 长度字段占用的字节数(例如4字节表示int) |
| 长度字段值后的内容长度调整(若长度字段包含自身长度,需调整) |
| 解析后跳过的字节数(例如跳过长度字段本身) |
3. 协议格式示例
假设自定义协议格式如下(长度字段在前):
+--------+----------------+
| Length | Actual Data |
| 4字节 | (变长内容) |
+--------+----------------+
4. 常见配置场景
场景1:长度字段包含自身
// 长度字段包含自身(如总长度= Length字段长度 + 数据长度)
new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0);
// lengthAdjustment = -4(扣除长度字段自身占用的4字节)
场景2:长度字段在消息中间
// 消息格式:[Header][Length][Data]
new LengthFieldBasedFrameDecoder(1024, 2, 4, 0, 6);
// lengthFieldOffset=2(跳过Header的2字节)
// initialBytesToStrip=6(跳过Header+Length字段)
5. 注意事项
- 参数匹配协议:必须与协议中长度字段的位置和计算方式一致。
- 编解码顺序:
LengthFieldBasedFrameDecoder
需作为第一个解码器添加到Pipeline。 - 异常处理:建议配合
ExceptionHandler
处理解码失败的情况。
通过这种方式,Netty 会自动根据长度字段切分完整的数据包,彻底解决粘包/拆包问题。
举个例子
完整示例:客户端与服务端交互流程
1. 服务端代码(含响应)
public class NettyServer {public static void main(String[] args) throws InterruptedException {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {// 添加长度字段解码器ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));// 字符串解码器ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));// 业务处理器(返回响应)ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("[Server] Received: " + msg);// 返回响应(添加长度前缀)ctx.writeAndFlush("ACK: " + msg);}});}});ChannelFuture future = bootstrap.bind(8080).sync();future.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
2. 客户端代码(含编码器)
public class NettyClient {public static void main(String[] args) throws InterruptedException {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {// 添加编码器(为消息添加长度前缀)ch.pipeline().addLast(new MessageToByteEncoder<String>() {@Overrideprotected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {byte[] bytes = msg.getBytes(CharsetUtil.UTF_8);out.writeInt(bytes.length); // 写入4字节长度字段out.writeBytes(bytes); // 写入实际数据}});// 响应解码器(与服务端解码器对称)ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));// 业务处理器(打印响应)ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("[Client] Received: " + msg);}});}});ChannelFuture future = bootstrap.connect("localhost", 8080).sync();// 发送两条测试消息(自动处理粘包)future.channel().writeAndFlush("Hello Netty");future.channel().writeAndFlush("Test Message");future.channel().closeFuture().sync();} finally {group.shutdownGracefully();}}
}
3. 执行流程说明
- 客户端发送消息:
-
- 编码器将字符串转换为
长度字段(4字节) + 实际数据
的二进制格式。 - 示例消息
"Hello Netty"
的传输格式:
- 编码器将字符串转换为
+----------+-----------------+
| 0x00000B | "Hello Netty" | // 0x00000B = 11字节(字符串长度)
+----------+-----------------+
- 服务端解析消息:
-
LengthFieldBasedFrameDecoder
根据长度字段切分完整数据包。StringDecoder
将二进制数据转为字符串,业务处理器打印并返回响应。
- 客户端接收响应:
-
- 服务端返回的
"ACK: Hello Netty"
同样通过长度字段编码。 - 客户端解码器解析后打印响应信息。
- 服务端返回的
4. 网络包结构示意图
客户端发送:
[Length=11][Data="Hello Netty"][Length=12][Data="Test Message"]服务端接收:
[Length=11][Data="Hello Netty"] → 完整解析为独立消息
[Length=12][Data="Test Message"] → 完整解析为独立消息服务端响应:
[Length=16][Data="ACK: Hello Netty"]
[Length=17][Data="ACK: Test Message"]
5. 关键点总结
- 编码对称性:客户端和服务端的编解码器需匹配(长度字段位置一致)。
- 自动分包:
LengthFieldBasedFrameDecoder
自动处理TCP流中的粘包/拆包。 - 性能保障:基于长度字段的解析效率极高,适合高频数据传输场景。
运行示例后,你将在控制台看到完整的请求-响应日志,验证粘包问题的解决效果。