物联网 基于netty理解粘包/拆包
简述
TCP 是流式协议,数据像水流一样到达。发送方连续发送多条消息时,接收方可能发生
粘包:多条消息合并成一条收到
拆包:一条消息被拆成多次收到
例如客户端发送 "Hello" 和 "Netty",服务器可能收到 "HelloNetty" 或 "Hel"+"loNetty"。
仅用 StringDecoder 无法还原原始消息边界
源码
netty-sample-00[https://gitee.com/kcnf-webrtc/iot-sample/tree/master/netty/netty-sample-00]
LengthFieldBasedFrameDecoder
- 核心参数(本例使用)
maxFrameLength:最大帧长度(防攻击)
lengthFieldOffset:长度字段偏移量(0,从开头)
lengthFieldLength:长度字段字节数(4,int 类型)
lengthAdjustment:长度字段补偿值(0)
initialBytesToStrip:剥离字节数(4,去掉长度字段,只保留数据)
粘包/拆包代码
server 端
package com.jysemel.iot;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.StandardCharsets;
public class EchoServerWithFrameDecoder {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
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) {
ChannelPipeline pipeline = ch.pipeline();
// ----- 关键点:解决粘包/拆包 -----
// 1. 长度字段解码器:最大帧长1024,长度字段从偏移0开始占4字节,不调整,剥离4字节长度字段
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
// 2. 将帧内容(字节)转为字符串
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
// 3. 出站编码(服务器向客户端写字符串时转为字节)
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
// 4. 业务处理器
pipeline.addLast(new EchoServerHandler());
}
});
ChannelFuture future = bootstrap.bind(8182).sync();
System.out.println("EchoServerWithFrameDecoder 启动,端口 8182");
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
client 端
package com.jysemel.iot;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.StandardCharsets;
public class EchoClientWithFrameDecoder {
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) {
ChannelPipeline pipeline = ch.pipeline();
// ----- 关键点:发送消息时自动添加4字节长度前缀 -----
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
pipeline.addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect("127.0.0.1", 8182).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
运行与验证
服务端收到: 消息_1
服务端收到: 消息_2
服务端收到: 消息_3
服务端收到: 消息_4
服务端收到: 消息_5
总结
| 组件 | 作用 |
|---|---|
| LengthFieldBasedFrameDecoder | 入站解码器,根据长度字段从 TCP 流中截取出一个个完整的数据帧,解决粘包/拆包。 |
| LengthFieldPrepender | 出站编码器,在消息前添加长度字段(通常与解码器参数对应)。 |
| StringDecoder | 将帧内的字节数组转为字符串(需保证帧内是完整的 UTF-8 字节)。 |
