常用的编解码器#
固定长度的解码器FixedLengthFrameDecoder
#
他里面只维护着一个private final int frameLength;
使用时,我们通过构造函数传递给他,他就会按照下面的方式解码
我们看一下它的javaDoc
原始数据 * +---+----+------+----+ * | A | BC | DEFG | HI | * +---+----+------+----+ 如果frameLength==3 * +-----+-----+-----+ * | ABC | DEF | GHI | * +-----+-----+-----+
它的decode()
实现如下
protected Object decode( @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception { if (in.readableBytes() < frameLength) { return null; } else { // 从in中截取 frameLength 长度的 字节流 return in.readRetainedSlice(frameLength); } }
行解码器LineBasedFrameDecoder
#
她会根据换行符进行解码, 无论用户发送过来的数据是以 \r\n 还是 \n 类型的换行符LineBasedFrameDecoder
使用:
public LineBasedFrameDecoder(final int maxLength) { this(maxLength, true, false); } public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) { this.maxLength = maxLength; this.failFast = failFast; this.stripDelimiter = stripDelimiter; }
第一个构造函数
- 入参位置是我们指定的每一行最大的字节数, 超过了这个大小的所有行,将全部被丢弃
- 默认跳过分隔符
- 出现了超过最大值的行,不报异常
第二个构造函数
- 入参1 是我们指定的每一行最大的字节数, 超过了这个大小的所有行,将全部被丢弃
- 入参2 指定每次解析是否跳过换行符
- 入参3 指定出现大于规定的最大字节数时是否报异常
看它重写的decode()
的实现逻辑如下:
它总起来分成四种情况
- 非丢弃模式
- 找到了换行符
- 如 readIndex + 换行符的位置 < maxLength 的关系 --> 解码
- 如 readIndex + 换行符的位置 > maxLength的关系 --> 丢弃
- 未找到换行符
- 如果可解析的长度 > maxLength --> 丢弃
- 丢弃模式
- 找到了换行符
- 丢弃
- 未找到换行符
- 丢弃
基于分隔符的解码器DelimiterBasedFrameDecoder
#
它主要有这几个成员变量, 根据这几个成员变量,可以选出使用它哪个构造函数
private final ByteBuf[] delimiters; 分隔符,数组 private final int maxFrameLength; 每次能允许的最大解码长度 private final boolean stripDelimiter; 是否跳过分隔符 private final boolean failFast; 超过最大解码长度时,是否抛出异常 private boolean discardingTooLongFrame; 是否丢弃超过最大限度的帧 private int tooLongFrameLength; 记录超过最大范围的字节数值
分三步
- 第一, 判断我们传递进入的分隔符是否是
\n \r\n
如果是的话,就是用上面的, 行解码器 - 第二步, 按照最细的力度进行解码, 比如, 我们有两个解码器, AB, 当前的readIndex 到A, 有2个字节, 到B有3个字节, 就会按照A进行解码
- 解码
基于长度域的解码器LengthFieldBasedFrameDecoder
#
通常我们在对特定的网络协议进行解码时会用到它,比如说,最典型的http协议, 虽然http协议看起来, 又有请求头,又有请求体,挺麻烦的,它在网络中依然是以字节流的方式进行传输
基于长度域,指的是在传输的协议中有一个 length字段,这个十六进制的字段记录的可能是整个协议的长度,也可能是消息体的长度, 我们根据具体情况使用不同的构造函数
如何使用呢? 最常用它下面的这个构造函数
public LengthFieldBasedFrameDecoder( int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) { this( maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true); }
使用它的前提是,知道这五个参数的意思
- maxFrameLength 每次解码所能接受的最大帧的长度
- lengthFieldOffset 长度域的偏移量
听着挺高大尚的, 偏移量, 说白了,就是在现有的这段字节数据中找个开始解码的位置, 大多数设为0, 意为,从o位置 开始解码
- lengthFieldLength 字段域的长度, 根据lengthFieldOffset的初始值往后数lengthFieldLength个字节,这段范围解析出来的数值 可能是 长度域的大小,也可能是整个协议的大小(包括header,body...) 根据不同的协议不同
- lengthAdjustment 矫正长度
- initialBytesToStrip 需要取出的长度
下面是javaDoc给的例子
基于长度的拆包
* BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes) * +--------+----------------+ +--------+----------------+ * | Length | Actual Content |----->| Length | Actual Content | * | 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" | * +--------+----------------+ +--------+----------------+ 这是最简单的情况, 假定 Length的长度就是后面的 真正需要解码的内容 现在的字节全部解码后是这样的 12HELLO, WORLD 我们要做的就是区分出 12和HELLO, WORLD * lengthFieldOffset = 0 * lengthFieldLength = 2 // todo 每两个字节 表示一个数据包 * lengthAdjustment = 0 * initialBytesToStrip = 0 意思就是: 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,就是后面的字段域的长度 00 0C ==> 12 这个12 意思就是 长度域的长度, 说白了 就是我们想要的 HELLO, WORLD 的长度 这样一算,就分开了
基于长度的阶段拆包
* BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes) * +--------+----------------+ +----------------+ * | Length | Actual Content |----->| Actual Content | * | 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" | * +--------+----------------+ +----------------+ 情况2: * lengthFieldOffset = 0 * lengthFieldLength = 2 // todo 每两个字节 表示一个数据包 * lengthAdjustment = 0 * initialBytesToStrip = 2 意思就是 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,就是后面的字段域的长度是 00 0C ==> 12 这个12 意思就是 长度域的长度, 说白了 就是我们想要的 HELLO, WORLD 的长度 然后, 从0开始 忽略 initialBytesToStrip, 就去除了 length ,只留下 HELLO, WORLD 有时, 在某些其他协议中, length field 可能代表是整个消息的长度, 包括消息头 在这种情况下,我们就得指定一个 非零的 lengthAdjustment 去调整 * BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) * +--------+----------------+ +--------+----------------+ * | Length | Actual Content |----->| Length | Actual Content | * | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" | * +--------+----------------+ +--------+----------------+ * lengthFieldOffset = 0 * lengthFieldLength = 2 // todo 每两个字节 表示一个数据包 * lengthAdjustment = -2 * initialBytesToStrip = 0 意思就是 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制,表示整个协议的长度 00 0C ==> 14 意味,协议全长 14 现在还是不能区分开 Length 和 Actual Content 公式: 数据包的长度 = 长度域 + lengthFieldOffset + lengthFieldLength +lengthAdjustment 通过他可以算出 lengthAdjustment = -2
基于偏移长度的拆包
* 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" | * +----------+----------+----------------+ +----------+----------+----------------+ 这个例子和第一个例子很像,但是多了头 我们想拿到后面消息长度的信息,就偏移过header * lengthFieldOffset = 2 * lengthFieldLength = 3 // todo 每两个字节 表示一个数据包 * lengthAdjustment = 0 * initialBytesToStrip = 0 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制, 表示长度域的长度 在这里 整好跳过了 header 1, 0x00 00 0C 是三个字节 也就是 字节数组[lengthFieldOffset,lengthFieldLength]=>[0,3] 0x00 00 0C == 12 表示长度域是 12 现在也成功区分开了 Header 1 和 Length 和 Actual Content 分别是 2 3 12
基于可调整长度的拆包
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" | * +----------+----------+----------------+ +----------+----------+----------------+ * lengthFieldOffset = 0 * lengthFieldLength = 3 // todo 每两个字节 表示一个数据包 * lengthAdjustment = 2 * initialBytesToStrip = 0 字节数组[lengthFieldOffset,lengthFieldLength]之间的内容转换成十进制, 表示长度域的长度 也就是 字节数组[lengthFieldOffset,lengthFieldLength]=>[0,3] 0x00 00 0C 是三个字节 0x00 00 0C == 12 表示长度域是 12 == 长度域的长度 就是 HELLO, WORLD的长度 但是上面的图多了一个 两个字节长度的 Header 1 下一步进行调整 公式: 数据包的长度 = 长度域 + lengthFieldOffset + lengthFieldLength +lengthAdjustment lengthAdjustment= 17-12-0-3=2
基于偏移可调整长度的截断拆包
* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) * +------+--------+------+----------------+ +------+----------------+ * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | * | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | * +------+--------+------+----------------+ +------+----------------+ * lengthFieldOffset = 1 * lengthFieldLength = 2 // todo 每两个字节 表示一个数据包 * lengthAdjustment = 1 * initialBytesToStrip = 3 lengthFieldOffset =1 偏移1字节 跨过 HDR1 lengthFieldLength =2 从[1,2] ==> 0x000C =12 表示长度域的值 看拆包后的结果,后面明显还多了个 HDR2 ,进行调整 公式: 数据包值 = 长度域 + lengthFieldOffset+ lengthFieldLength + lengthAdjustment 算出 lengthAdjustment = 16 - 12 - 1 - 2 = 1 结果值只有 HDR2 和 Actual Content , 说明,前面通过 initialBytesToStrip 进行忽略 initialBytesToStrip =3
基于偏移可调整长度的 变种 截断拆包
* BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) * +------+--------+------+----------------+ +------+----------------+ * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | * +------+--------+------+----------------+ +------+----------------+ * lengthFieldOffset = 1 * lengthFieldLength = 2 // todo 每两个字节 表示一个数据包 * lengthAdjustment = -3 * initialBytesToStrip = 3 同样 看结果,保留 HDR2 和 Actual Content lengthFieldOffset = 1 表示跳过开头的 HDR1 [1,2] ==> 00 10 , 算出的 长度域的值==10 很显然这不对 10 < 13 我们要想拆出后面的数据包就得在现有的基础上往左移动三个字节 -3个调整量