目录
如何使用?
以下是使用 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>() {
@Override
protected void initChannel(SocketChannel ch) {
// 关键:添加 LengthFieldBasedFrameDecoder
ch.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>() {
@Override
protected 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>() {
@Override
protected 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>() {
@Override
protected 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>() {
@Override
protected void initChannel(SocketChannel ch) {
// 添加编码器(为消息添加长度前缀)
ch.pipeline().addLast(new MessageToByteEncoder<String>() {
@Override
protected 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>() {
@Override
protected 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流中的粘包/拆包。 - 性能保障:基于长度字段的解析效率极高,适合高频数据传输场景。
运行示例后,你将在控制台看到完整的请求-响应日志,验证粘包问题的解决效果。