1. 什么是TCP粘包
?
1.1 粘包的定义
TCP(传输控制协议)是一种面向流的协议,它不保留消息边界。发送方多次写入的数据可能会被接收方一次性读取,这种现象称为粘包(Sticky Packet)
。
粘包不是TCP协议的缺陷,而是其设计特性导致的。
1.2 粘包的场景
- 发送方粘包:发送方频繁发送小数据包,TCP可能合并发送以优化性能。
- 接收方粘包:接收方缓冲区未及时读取,导致多个包被一次性读取。
粘包情况模拟
服务端代码(不处理粘包)
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) {
ch.pipeline().addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 直接打印接收到的数据(未处理粘包)
System.out.println("服务端收到: " + msg.toString(CharsetUtil.UTF_8));
}
});
}
});
ChannelFuture future = bootstrap.bind(8080).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端代码(连续发送小数据包)
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 ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 连续发送3条消息
for (int i = 0; i < 3; i++) {
ByteBuf buf = Unpooled.copiedBuffer("消息" + i, CharsetUtil.UTF_8);
ctx.writeAndFlush(buf);
}
}
});
}
});
ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
运行结果
客户端发送了3条独立消息:
消息0、消息1、消息2
但服务端可能一次性收到合并后的数据:
服务端收到: 消息0消息1消息2
这就是典型的粘包问题
!
1.3 粘包的危害
- 数据解析错误(如协议头尾混淆)。
- 消息丢失或重复处理。
3. 用Netty解决粘包问题
3.1 解决方案
Netty提供了多种拆包策略,常见的有:
固定长度拆包
(FixedLengthFrameDecoder)
- 每条消息固定长度,不足补空。
分隔符拆包
(DelimiterBasedFrameDecoder)
- 用特殊字符(如\n)分隔消息。
长度字段拆包
(LengthFieldBasedFrameDecoder)
- 在消息头中定义长度字段(
推荐
)。
3.2 代码改造(使用LengthFieldBasedFrameDecoder)
服务端代码(解决粘包)
ch.pipeline()
// 最大长度、长度字段偏移量、长度字段长度
.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4))
.addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 现在每条消息会被正确拆分
System.out.println("服务端收到: " + msg.toString(CharsetUtil.UTF_8));
}
});
客户端代码(添加长度头)
@Override
public void channelActive(ChannelHandlerContext ctx) {
for (int i = 0; i < 3; i++) {
String message = "消息" + i;
ByteBuf buf = Unpooled.buffer();
// 写入消息长度(4字节)
buf.writeInt(message.getBytes().length);
// 写入消息内容
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
3.3 运行结果
服务端现在能正确接收每条独立消息:
服务端收到: 消息0
服务端收到: 消息1
服务端收到: 消息2
4. 其他拆包方案对比
方案 | 优点 | 缺点 |
---|---|---|
FixedLengthFrameDecoder | 简单高效 | 消息必须固定长度 |
DelimiterBasedFrameDecoder | 适合文本协议(如HTTP) | 分隔符不能出现在消息体中 |
LengthFieldBasedFrameDecoder | 灵活,适合二进制协议 | 需要自定义长度字段 |
5. 总结
粘包本质:TCP流式传输的特性,需应用层自行处理消息边界。
Netty解决方案:
- 简单场景:用
DelimiterBasedFrameDecoder
(如换行符分隔)。 - 复杂场景:用
LengthFieldBasedFrameDecoder
(推荐)。
关键点:
- 客户端和服务端的编解码器必须匹配。
- 长度字段需明确(如4字节int)。
通过合理选择拆包策略,可以彻底解决TCP粘包问题!