问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Netty源码阅读入门实战(八) - 解码下

创作时间:
作者:
@小白创作中心

Netty源码阅读入门实战(八) - 解码下

引用
1
来源
1.
https://cloud.tencent.com/developer/article/1790725

本文原文发布于2021年2月,是一篇关于Netty源码阅读的技术文章,主要讲解了Netty中的解码器原理和使用方法。文章内容深入,包含了具体的代码分析和参数解释,适合有一定Java和网络编程基础的开发者阅读。

基于分隔符解码器分析

  • 构造器 传入一系列分隔符,通过解码器将二进制流分成完整数据包
  • decode 方法

5.1 分析解码步骤

5.1.1 行处理器

  • 行处理器决断
  • 定义位置

  • 初始化位置
  • 判断分隔符

5.1.2 找到最小分隔符

遍历所有分隔符,计算以每一个分隔符分割的数据包的长度

5.1.3 解码

5.1.3.1 找到分隔符

非空,说明已经找到分隔符 和之前一样,在此先判断当前是否处于丢弃模式
非丢弃模式
显然第一次时为 false, 因此非丢弃模式

当前数据包大于允许解析最大数据长度时,直接将该段数据包连同最小分隔符跳过(丢弃)
没有超过的就是正常合理逻辑的数据包的长度,判断解析出的数据包是否包含分隔符
丢弃模式

5.1.3.2 未找到分隔符
5.1.3.2.1 非丢弃模式

当前可读字节长大于允许解析最大数据长度时,记录该丢弃字节数
5.1.3.2.2 丢弃模式

基于长度域解码器参数分析

重要参数

  • maxFrameLength (包的最大长度) 防止太大导致内存溢出,超出包的最大长度 Netty 将会做一些特殊处理
  • lengthFieldOffset (消息体长度) 长度域的偏移量lengthFieldOffset,0表示无偏移
    ByteBuf
    的什么位置开始就是
    length
    字段
  • lengthFieldLength 长度域
    length
    字段的长度
  • lengthAdjustment 有些情况可能会把header也包含到length长度中,或者length字段后面还有一些不包括在length长度内的,可以通过lengthAdjustment调节
  • initialBytesToStrip 起始截掉的部分,如果传递给后面的Handler的数据不需要消息头了,可以通过这个设置 可以通过消息中的一个表示消息长度的字段值动态分割收到的ByteBuf

基于长度

这类数据包协议比较常见,前几个字节表示数据包长度(不包括长度域),后面为具体数据 拆完后数据包是一个完整的带有长度域的数据包(之后即可传递到应用层解码器进行解码), 创建一个如下方式的
LengthFieldBasedFrameDecoder
即可实现这类协议

6.2 基于长度截断

若应用层解码器不需用到长度字段,那么我们希望 Netty 拆包后,如此
长度域被截掉,我们只需指定另一个参数
initialBytesToStrip
即可实现 表 Netty 拿到一个完整数据包后向业务解码器传递之前,应该跳过多少字节
initialBytesToStrip
为4,表获取一个完整数据包后,忽略前面4个字节,应用解码器拿到的就是
不带长度域
的数据包

6.3 基于偏移长度

此方式二进制协议更为普遍,前几个固定字节表示协议头,通常包含一些
magicNumber

protocol version
之类的
meta
信息,紧跟着后面的是一个长度域,表示包体有多少字节的数据 只需要基于第一种情况,调整第二个参数既可以实现
lengthFieldOffset
为4,表示跳过4个字节才是长度域。

6.4 基于可调整长度的拆包

有的二进制协议会设计成如下方式
长度域在前,
header
在后

  • 长度域在数据包最前面表示无偏移,
    lengthFieldOffset = 0
  • 长度域的长度为3,即
    lengthFieldLength = 3
  • 长度域表示的包体的长度略过了header,这里有另外一个参数
    lengthAdjustment
    ,包体长度调整的大小,长度域的数值表示的长度加上这个修正值表示的就是带header的包,这里是
    12+2
    ,header和包体一共占14字节

6.5 基于偏移可调整长度的截断

二进制协议带有两个header
拆完后,
HDR1
丢弃,长度域丢弃,只剩下第二个
header
和有效包体
这种协议中,一般
HDR1
可以表示
magicNumber
,表示应用只接受以该
magicNumber
开头的二进制数据,RPC 里面用的较多

参数设置

  • 长度域偏移为1,即
    lengthFieldOffset
    为1
  • 长度域长度为2,即
    lengthFieldLength
    为2
  • 长度域表示的包体的长度略过
    HDR2
    ,但拆包时
    HDR2
    也被 Netty 当作包体的一部分来拆,
    HDR2
    的长度为1,即
    lengthAdjustment
    为1
  • 拆完后,截掉前面三个字节,即
    initialBytesToStrip
    为 3

6.6 基于偏移可调整变异长度的截断

前面所有的长度域表示的都是不带
header
的包体的长度
如果让长度域表示的含义包含整个数据包的长度,如下
长度域字段值为16,
其字段长度为2,
HDR1
的长度为1,
HDR2
的长度为1,包体的长度为12,
1+1+2+12=16

参数设置

除长度域表示的含义和上一种情况不一样外,其他都相同,因为 Netty 不了解业务情况,需告诉 Netty ,长度域后再跟多少字节就可形成一个完整数据包,这里显然是13字节,长度域为16,因此减掉3才是真是的拆包所需要的长度,
lengthAdjustment
为-3
若你的协议基于长度,即可考虑不用字节来实现,而是直接拿来用,或者继承他,简单修改即可

7 基于长度域解码器分析

7.1 构造方法

代码语言:javascript
代码运行次数:0

public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
    // 省略参数校验
    this.byteOrder = byteOrder;
    this.maxFrameLength = maxFrameLength;
    this.lengthFieldOffset = lengthFieldOffset;
    this.lengthFieldLength = lengthFieldLength;
    this.lengthAdjustment = lengthAdjustment;
    lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
    this.initialBytesToStrip = initialBytesToStrip;
    this.failFast = failFast;
}

把传参数保存在 field即可

  • byteOrder 字节流表示的数据是大端还是小端,用于长度域的读取
  • lengthFieldEndOffset 紧跟长度域字段后面的第一个字节的在整个数据包中的偏移量
  • failFast
  • 为true 表读取到长度域,TA的值的超过
    maxFrameLength
    ,就抛
    TooLongFrameException

  • false
    表只有当真正读取完长度域的值表示的字节之后,才抛
    TooLongFrameException
    ,默认设为
    true
    ,建议不要修改,否则可能会造成内存溢出

7.2 实现拆包抽象

具体的拆包协议只需要实现
代码语言:javascript
代码运行次数:0

void decode(ChannelHandlerContext ctx, ByteBuf in, List out)

in 表目前为止还未拆的数据,拆完之后的包添加到 out这个list中即可实现包向下传递
第一层实现
重载的protected方法decode实现真正的拆包,以下三步走
基于长度域解码器步骤
计算需要抽取的数据包长度跳过字节逻辑处理丢弃模式下的处理
1 计算需要抽取的数据包的长度

protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    // 拿到实际的未调整过的包长度
    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);
    if (frameLength < lengthFieldEndOffset) {
        failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
    }
    if (frameLength > maxFrameLength) {
        exceededFrameLength(in, frameLength);
        return null;
    }
}

拿到长度域的实际字节偏移
调整包的长度
如果当前可读字节还未达到长度长度域的偏移,那说明肯定是读不到长度域的,直接不读

上面有个getUnadjustedFrameLength,若你的长度域代表的值表达的含义不是基本的int,short等基本类型,可重写该方法

比如,有的奇葩的长度域里面虽然是4个字节,比如 0x1234,但是TA的含义是10进制,即长度就是十进制的1234,那么覆盖这个函数即可实现奇葩长度域拆包
2 长度校验
整个数据包的长度还没有长度域长,直接抛异常

数据包长度超出最大包长度,进入丢弃模式

当前可读字节已达到frameLength,直接跳过frameLength个字节,丢弃之后,后面有可能就是一个合法的数据包当前可读字节未达到frameLength,说明后面未读到的字节也需丢弃,进入丢弃模式,先把当前累积的字节全部丢弃
bytesToDiscard 表还需丢弃多少字节

最后,调用failIfNecessary判断是否需要抛出异常
不需要再丢弃后面的未读字节(bytesToDiscard == 0),重置丢弃状态
如果设置了快速失败(!failFast),或者设置了快速失败并且是第一次检测到大包错误(firstDetectionOfTooLongFrame),抛出异常,让handler处理如果设置了快速失败,并且是第一次检测到打包错误,抛出异常,让handler去处理

前面我们可以知道failFast默认为true,而这里firstDetectionOfTooLongFrame为true,所以,第一次检测到大包肯定会抛出异常

3 丢弃模式的处理
LengthFieldBasedFrameDecoder.decoder方法入口处还有一段代码

若当前处在丢弃模式,先计算需要丢弃多少字节,取当前还需可丢弃字节和可读字节的最小值,丢弃后,进入 failIfNecessary,对照着这个函数看,默认情况下是不会继续抛出异常,而如果设置了 failFast为false,那么等丢弃完之后,才会抛出异常

2 跳过指定字节长度的逻辑处理
在丢弃模式的处理及长度校验都通过后
先验证当前是否已读到足够的字节,若读到了,在下一步抽取一个完整的数据包之前,需根据initialBytesToStrip的设置来跳过某些字节,当然,跳过的字节不能大于数据包的长度,否则抛 CorruptedFrameException 异常

抽取frame
拿到当前累积数据的读指针,然后拿到待抽取数据包的实际长度进行抽取,抽取之后,移动读指针
抽取的过程即调用了一下 ByteBuf 的retainedSlice API,该API无内存copy的开销

从真正抽取数据包来看看,传入的参数为 int 型,所以自定义协议中,如果你的长度域是8字节,那么前4字节基本没用
小结
如果你使用了Netty,并且二进制协议基于长度,考虑使用LengthFieldBasedFrameDecoder吧,通过调整各种参数,一定会满足你LengthFieldBasedFrameDecoder的拆包包括合法参数校验,异常包处理,以及最后调用 ByteBuf 的retainedSlice来实现无内存copy的拆包
8 解码器总结
8.1 ByteToMessageDecoder 解码步骤
累加字节流调用子类的decode方法进行解析将解析到的ByteBuf向下传播
8.2 基于长度解码器步骤
计算需要抽取的数据包长度跳过字节逻辑处理丟弃模式下的处理
8.3 两个问题
解码器抽象的解码过程netty里面有哪些拆箱即用的解码器

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号