Netty拆粘包问题
TCP 协议传输的过程:

发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。发送两个完整包到接收端的时候:

以下情况:

由发送的两个报文组成的,对于应用程序来说就很难处理了(这样称为粘包)。原因:
应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
传输层:滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大(大于256 bytes),这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包

里面的内容却是互相包含,对于应用来说依然无法解析(半包)。原因:
应用层:接收方 ByteBuf 小于实际发送数据量
传输层:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时接收方窗口中无法容纳发送方的全部报文,发送方只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
数据链路层:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
本质
发生粘包与半包现象的本质是因为 TCP 是流式协议,消息无边界
解决方案
-
行解码器
通过分隔符对数据进行拆分来解决粘包半包问题,通过LineBasedFrameDecoder(int maxLength)来拆分以换行符(\n)为分隔符的数据,也可以通过DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf... delimiters)来指定通过什么分隔符来拆分数据(可以传入多个分隔符)
两种解码器都需要传入数据的最大长度,若超出最大长度,会抛出TooLongFrameException异常
// 通过行解码器对粘包数据进行拆分,以 \n 为分隔符 // 需要指定最大长度 ch.pipeline().addLast(new DelimiterBasedFrameDecoder(64)); ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
客户端代码
// 约定最大长度为 64 final int maxLength = 64; // 被发送的数据 char c = 'a'; for (int i = 0; i < 10; i++) { ByteBuf buffer = ctx.alloc().buffer(maxLength); // 生成长度为0~62的数据 Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int j = 0; j < (int)(random.nextInt(maxLength-2)); j++) { sb.append(c); } // 数据以 \n 结尾 sb.append("\n"); buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8)); c++; // 将数据发送给服务器 ctx.writeAndFlush(buffer); }
运行结果
4184 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x9d6ac701, L:/127.0.0.1:8080 - R:/127.0.0.1:58282] READ: 10B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaa | +--------+-------------------------------------------------+----------------+ 4184 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x9d6ac701, L:/127.0.0.1:8080 - R:/127.0.0.1:58282] READ: 11B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 62 62 62 62 62 62 62 62 62 62 62 |bbbbbbbbbbb | +--------+-------------------------------------------------+----------------+ 4184 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x9d6ac701, L:/127.0.0.1:8080 - R:/127.0.0.1:58282] READ: 2B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 63 63 |cc | +--------+-------------------------------------------------+----------------+
以自定义分隔符 \c 为分隔符
客户端代码
// 数据以 \c 结尾 sb.append("\\c"); buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8));
服务器代码
// 将分隔符放入ByteBuf中 ByteBuf bufSet = ch.alloc().buffer().writeBytes("\\c".getBytes(StandardCharsets.UTF_8)); // 通过行解码器对粘包数据进行拆分,以 \c 为分隔符 ch.pipeline().addLast(new DelimiterBasedFrameDecoder(64, ch.alloc().buffer().writeBytes(bufSet))); ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
运行结果
8246 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x86215ccd, L:/127.0.0.1:8080 - R:/127.0.0.1:65159] READ: 14B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaa | +--------+-------------------------------------------------+----------------+ 8247 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x86215ccd, L:/127.0.0.1:8080 - R:/127.0.0.1:65159] READ: 3B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 62 62 62 |bbb | +--------+-------------------------------------------------+----------------+
-
定长解码
客户端于服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度,服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder对数据进行定长解码
// 通过定长解码器对粘包数据进行拆分 ch.pipeline().addLast(new FixedLengthFrameDecoder(16)); ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
客户端发送数据的代码如下
// 约定最大长度为16 final int maxLength = 16; // 被发送的数据 char c = 'a'; // 向服务器发送10个报文 for (int i = 0; i < 10; i++) { ByteBuf buffer = ctx.alloc().buffer(maxLength); // 定长byte数组,未使用部分会以0进行填充 byte[] bytes = new byte[maxLength]; // 生成长度为0~15的数据 for (int j = 0; j < (int)(Math.random()*(maxLength-1)); j++) { bytes[j] = (byte) c; } buffer.writeBytes(bytes); c++; // 将数据发送给服务器 ctx.writeAndFlush(buffer); }
结果
8222 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xbc122d07, L:/127.0.0.1:8080 - R:/127.0.0.1:52954] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 61 61 61 61 00 00 00 00 00 00 00 00 00 00 00 00 |aaaa............| +--------+-------------------------------------------------+----------------+ 8222 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xbc122d07, L:/127.0.0.1:8080 - R:/127.0.0.1:52954] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 62 62 62 00 00 00 00 00 00 00 00 00 00 00 00 00 |bbb.............| +--------+-------------------------------------------------+----------------+ 8222 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xbc122d07, L:/127.0.0.1:8080 - R:/127.0.0.1:52954] READ: 16B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 63 63 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |cc..............| +--------+-------------------------------------------------+----------------+
-
长度字段解码器
LengthFieldBasedFrameDecoder解码器可以提供更为丰富的拆分方法,其构造方法有五个参数
public LengthFieldBasedFrameDecoder( int maxFrameLength, // 数据最大长度 int lengthFieldOffset, // 数据长度标识的起始偏移量
int lengthFieldLength, // 数据长度标识所占字节数 int lengthAdjustment, //长度表示与有用数据的偏移量
int initialBytesToStrip) // 数据读取起点
参数图解:
lengthFieldOffset = 0 lengthFieldLength = 2 lengthAdjustment = 0 initialBytesToStrip = 0 (= do not strip header) BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) +--------+----------------+ +--------+----------------+ | Length | Actual Content |----->| Length | Actual Content | | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | +--------+----------------+ +--------+----------------+
0x000C 即为后面 HELLO, WORLD的长度
lengthFieldOffset = 0 lengthFieldLength = 2 lengthAdjustment = 0 initialBytesToStrip = 2 (= the length of the Length field) BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes) +--------+----------------+ +----------------+ | Length | Actual Content |----->| Actual Content | | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" | +--------+----------------+ +----------------+
从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识),因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO, WORLD
lengthFieldOffset = 2 (= the length of Header 1) lengthFieldLength = 3 lengthAdjustment = 0 initialBytesToStrip = 0 BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) +----------+----------+----------------+ +----------+----------+----------------+ | Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content | | 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" | +----------+----------+----------------+ +----------+----------+----------------+
长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C)
Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度
lengthFieldOffset = 0 lengthFieldLength = 3 lengthAdjustment = 2 (= the length of Header 1) initialBytesToStrip = 0 BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) +----------+----------+----------------+ +----------+----------+----------------+ | Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content | | 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" | +----------+----------+----------------+ +----------+----------+----------------+
从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE),长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO, WORLD,不包括0xCAFE
Protobuf粘包拆包处理
//拆包解码 .addLast(new ProtobufVarint32FrameDecoder()) .addLast(new ProtobufVarint32LengthFieldPrepender())
简单理解为是在消息体中加了一个 32 位长度的整形字段,用于表明当前消息长度。
浙公网安备 33010602011771号