李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
03.Netty进阶之长度域解码器
Leefs
2022-06-15 AM
1753℃
0条
[TOC] ### 一、概述 长度域解码器(LengthFieldBasedFrameDecoder)是解决 TCP 拆包/粘包问题最常用的**解码器**。 它基本上可以覆盖大部分基于长度拆包场景,开源消息中间件 RocketMQ 就是使用长度域解码器进行解码的。 **长度域解码器**相比**固定长度解码器**和**特殊分隔符解码器**要复杂一些,接下来我们就一起学习下这个强大的解码器。 ### 二、重要属性 + **长度域解码器特有属性** ```java // 长度字段的偏移量,也就是存放长度数据的起始位置 private final int lengthFieldOffset; // 长度字段所占用的字节数 private final int lengthFieldLength; /* * 消息长度的修正值 * 在很多较为复杂一些的协议设计中,长度域不仅仅包含消息的长度,而且包含其他的数据,如版本号、数据类型、数据状态等,那么这时候我们需要使用 lengthAdjustment 进行修正 * lengthAdjustment = 包体的长度值 - 长度域的值 */ private final int lengthAdjustment; // 解码后需要跳过的初始字节数,也就是消息内容字段的起始位置 private final int initialBytesToStrip; // 长度字段结束的偏移量,lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength private final int lengthFieldEndOffset; ``` + **与固定长度解码器和特定分隔符解码器相似的属性** ```java private final int maxFrameLength; // 报文最大限制长度 private final boolean failFast; // 是否立即抛出 TooLongFrameException,与 maxFrameLength 搭配使用 private boolean discardingTooLongFrame; // 是否处于丢弃模式 private long tooLongFrameLength; // 需要丢弃的字节数 private long bytesToDiscard; // 累计丢弃的字节数 ``` ### 三、具体示例 在 Netty LengthFieldBasedFrameDecoder 源码的注释中一共给出了7个场景示例 #### 示例一 > 典型的基于消息长度 + 消息内容的解码 ``` BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) +--------+----------------+ +--------+----------------+ | Length | Actual Content |----->| Length | Actual Content | | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | +--------+----------------+ +--------+----------------+ ``` 上述协议是最基本的格式,报文只包含消息长度 Length 和消息内容 Content 字段,其中 Length 为 16 进制表示,共占用 2 字节,Length 的值 0x000C 代表 Content 占用 12 字节。 **该协议对应的解码器参数组合如下:** - lengthFieldOffset = 0,因为 Length 字段就在报文的开始位置。 - lengthFieldLength = 2,协议设计的固定长度。 - lengthAdjustment = 0,Length 字段只包含消息长度,不需要做任何修正。 - initialBytesToStrip = 0,解码后内容依然是 Length + Content,不需要跳过任何初始字节。 #### 示例二 > 解码结果需要截断 ``` BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes) +--------+----------------+ +----------------+ | Length | Actual Content |----->| Actual Content | | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" | +--------+----------------+ +----------------+ ``` 示例 2 和示例 1 的区别在于解码后的结果只包含消息内容,其他的部分是不变的。 **该协议对应的解码器参数组合如下:** - lengthFieldOffset = 0,因为 Length 字段就在报文的开始位置。 - lengthFieldLength = 2,协议设计的固定长度。 - lengthAdjustment = 0,Length 字段只包含消息长度,不需要做任何修正。 - initialBytesToStrip = 2,跳过 Length 字段的字节长度,解码后 ByteBuf 中只包含 Content字段。 #### 示例三 > 长度字段包含消息长度和消息内容所占的字节 ``` BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) +--------+----------------+ +--------+----------------+ | Length | Actual Content |----->| Length | Actual Content | | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" | +--------+----------------+ +--------+----------------+ ``` 与前两个示例不同的是,示例 3 的 Length 字段包含 Length 字段自身的固定长度以及 Content 字段所占用的字节数,Length 的值为 0x000E(2 + 12 = 14 字节),在 Length 字段值(14 字节)的基础上做 lengthAdjustment(-2)的修正,才能得到真实的 Content 字段长度。 - lengthFieldOffset = 0,因为 Length 字段就在报文的开始位置。 - lengthFieldLength = 2,协议设计的固定长度。 - lengthAdjustment = -2,长度字段为 14 字节,需要减 2 才是拆包所需要的长度。 - initialBytesToStrip = 0,解码后内容依然是 Length + Content,不需要跳过任何初始字节。 #### 示例四 > 基于长度字段偏移的解码 ``` 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" | +----------+----------+----------------+ +----------+----------+----------------+ ``` 示例4中 Length 字段不再是报文的起始位置,Length 字段的值为 0x00000C,表示 Content 字段占用 12 字节。 **该协议对应的解码器参数组合如下:** - lengthFieldOffset = 2,需要跳过 Header 1 所占用的 2 字节,才是 Length 的起始位置。 - lengthFieldLength = 3,协议设计的固定长度。 - lengthAdjustment = 0,Length 字段只包含消息长度,不需要做任何修正。 - 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" | +----------+----------+----------------+ +----------+----------+----------------+ ``` 示例5中的 Length 字段之后是 Header 1,Length 与 Content 字段不再相邻。Length 字段所表示的内容略过了 Header 1 字段,所以也需要通过 lengthAdjustment 修正才能得到 Header + Content 的内容。 **示例5所对应的解码器参数组合如下:** - lengthFieldOffset = 0,因为 Length 字段就在报文的开始位置。 - lengthFieldLength = 3,协议设计的固定长度。 - lengthAdjustment = 2,由于 Header + Content 一共占用 2 + 12 = 14 字节,所以 Length 字段值(12 字节)加上 lengthAdjustment(2 字节)才能得到 Header + Content 的内容(14 字节)。 - initialBytesToStrip = 0,解码后内容依然是完整的报文,不需要跳过任何初始字节。 #### 示例六 > 基于长度偏移和长度修正的解码 ``` BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) +------+--------+------+----------------+ +------+----------------+ | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | +------+--------+------+----------------+ +------+----------------+ ``` 示例 6 中 Length 字段前后分为 HDR1 和 HDR2 字段,各占用 1 字节,所以既需要做长度字段的偏移,也需要做 lengthAdjustment 修正,具体修正的过程与示例5类似。 **对应的解码器参数组合如下:** - lengthFieldOffset = 1,需要跳过 HDR1 所占用的 1 字节,才是 Length 的起始位置。 - lengthFieldLength = 2,协议设计的固定长度。 - lengthAdjustment = 1,由于 HDR2 + Content 一共占用 1 + 12 = 13 字节,所以 Length 字段值(12 字节)加上 lengthAdjustment(1)才能得到 HDR2 + Content 的内容(13 字节)。 - initialBytesToStrip = 3,解码后跳过 HDR1 和 Length 字段,共占用 3 字节。 #### 示例七 > 长度字段包含除 Content 外的多个其他字段 ``` BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) +------+--------+------+----------------+ +------+----------------+ | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | +------+--------+------+----------------+ +------+----------------+ ``` 示例 7 与 示例 6 的区别在于 Length 字段记录了整个报文的长度,包含 Length 自身所占字节、HDR1 、HDR2 以及 Content 字段的长度,解码器需要知道如何进行 lengthAdjustment 调整,才能得到 HDR2 和 Content 的内容。 **所以可以采用如下的解码器参数组合:** - lengthFieldOffset = 1,需要跳过 HDR1 所占用的 1 字节,才是 Length 的起始位置。 - lengthFieldLength = 2,协议设计的固定长度。 - lengthAdjustment = -3,Length 字段值(16 字节)需要减去 HDR1(1 字节) 和 Length 自身所占字节长度(2 字节)才能得到 HDR2 和 Content 的内容(1 + 12 = 13 字节)。 - initialBytesToStrip = 3,解码后跳过 HDR1 和 Length 字段,共占用 3 字节。 ### 四、代码示例 > 通过EmbeddedChannel对handler进行测试 ```java import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; /** * @author lilinchao * @date 2022/6/15 * @description 通过EmbeddedChannel对handler进行测试 **/ public class TestLengthFieldDecoder { public static void main(String[] args) { // 模拟服务器 // 使用EmbeddedChannel测试handler EmbeddedChannel channel = new EmbeddedChannel( new LengthFieldBasedFrameDecoder( 1024, 0, 4, 1,4), new LoggingHandler(LogLevel.DEBUG) ); // 模拟客户端,写入4个字节的内容长度, 实际内容 ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); send(buffer, "Hello, world"); send(buffer, "Hi!"); channel.writeInbound(buffer); } private static void send(ByteBuf buffer, String content){ //实际内容 byte[] bytes = content.getBytes(); //实际内容长度 int length = bytes.length; // 写入数据长度标识 buffer.writeInt(length); // 写入长度标识后的其他信息 buffer.writeByte(1); // 写入具体的数据 buffer.writeBytes(bytes); } } ``` **运行结果** ``` 10:19:37.444 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 13B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |.Hello, world | +--------+-------------------------------------------------+----------------+ 10:19:37.445 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 4B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 01 48 69 21 |.Hi! | +--------+-------------------------------------------------+----------------+ 10:19:37.446 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE ``` *附参考文章链接:* *[《Netty 支持哪些常用的解码器》](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/08%20%20%E5%BC%80%E7%AE%B1%E5%8D%B3%E7%94%A8%EF%BC%9ANetty%20%E6%94%AF%E6%8C%81%E5%93%AA%E4%BA%9B%E5%B8%B8%E7%94%A8%E7%9A%84%E8%A7%A3%E7%A0%81%E5%99%A8%EF%BC%9F.md)*
标签:
Netty
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2166.html
上一篇
02.Netty进阶之粘包与半包解决方案
下一篇
04.Netty进阶之协议设计与解析
评论已关闭
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
NLP
4
标签云
Spark RDD
并发线程
稀疏数组
字符串
Ubuntu
Kafka
Shiro
队列
GET和POST
JavaScript
Kibana
SpringCloudAlibaba
Zookeeper
CentOS
容器深入研究
Spark Streaming
Docker
Jenkins
JavaSE
ClickHouse
Python
SpringCloud
哈希表
递归
工具
排序
Java阻塞队列
Scala
Spark SQL
MySQL
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭