标签搜索

目 录CONTENT

文章目录

Netty解码器 之 长度域解码器 LengthFieldBasedFrameDecoder

沙漠渔
2022-08-03 23:58:42 / 0 评论 / 0 点赞 / 1,166 阅读 / 8,999 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-08-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Netty是优秀的NIO通信框架,相比Mina(一个老同事使用该框架开发过一个应用,也是很牛逼的),但是我却对Netty总是有一种莫名的执着,包括自己写一些小应用、开发项目时基本都使用这个框架。

前言

Netty 已经封装好了网络通信的底层实现,应用开发只需要扩展 ChannelHandler 实现自定义编解码逻辑即可。Netty 提供了很多开箱即用的解码器,这些解码器基本覆盖了 TCP 拆包/粘包解决方案。

主要包括:

  • 固定长度解码器 FixedLengthFrameDecoder
  • 特殊分隔符解码器 DelimiterBasedFrameDecoder
  • 长度域解码器 LengthFieldBasedFrameDecoder

这里主要简单说明一下长度域解码器 LengthFieldBasedFrameDecoder, 因为基本可以覆盖了日常的通信协议(好像暴露了工作内容),另外的两种在工作中应用的相对较少,并且不灵活,暂时不进行说明。

源码解读

首先看其构造函数,了解其关键参数及属性。

    private final ByteOrder byteOrder;
    private final int maxFrameLength;
    private final int lengthFieldOffset;
    private final int lengthFieldLength;
    private final int lengthFieldEndOffset;
    private final int lengthAdjustment;
    private final int initialBytesToStrip;
    private final boolean failFast;
    private boolean discardingTooLongFrame;
    private long tooLongFrameLength;
    private long bytesToDiscard;
	
    public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) {
        this(maxFrameLength, lengthFieldOffset, lengthFieldLength, 0, 0);
    }

    public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
        this(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true);
    }

    public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        this(ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
    }

    public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        this.byteOrder = (ByteOrder)ObjectUtil.checkNotNull(byteOrder, "byteOrder");
        ObjectUtil.checkPositive(maxFrameLength, "maxFrameLength");
        ObjectUtil.checkPositiveOrZero(lengthFieldOffset, "lengthFieldOffset");
        ObjectUtil.checkPositiveOrZero(initialBytesToStrip, "initialBytesToStrip");
        if (lengthFieldOffset > maxFrameLength - lengthFieldLength) {
            throw new IllegalArgumentException("maxFrameLength (" + maxFrameLength + ") must be equal to or greater than lengthFieldOffset (" + lengthFieldOffset + ") + lengthFieldLength (" + lengthFieldLength + ").");
        } else {
            this.maxFrameLength = maxFrameLength;
            this.lengthFieldOffset = lengthFieldOffset;
            this.lengthFieldLength = lengthFieldLength;
            this.lengthAdjustment = lengthAdjustment;
            this.lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
            this.initialBytesToStrip = initialBytesToStrip;
            this.failFast = failFast;
        }
    }

通过上面的源码,可以看到,其共有11个属性,其中包括6个解码器通用属性,5个特有属性。
通用属性包括:

  • byteOrder 大端还是小端报文
  • maxFrameLength 报文最大限制长度
  • failFast 是否立即抛出 TooLongFrameException,与 maxFrameLength 搭配使用
  • discardingTooLongFrame 是否处于丢弃模式
  • tooLongFrameLength 需要丢弃的字节数
  • bytesToDiscard 累计丢弃的字节数

特有属性包括:

  • lengthFieldOffset 长度字段的偏移量,也就是存放长度数据的起始位置。
  • lengthFieldLength 长度字段所占用的字节数。
  • lengthAdjustment 消息长度的修正值,lengthAdjustment = 包体的长度值 - 长度域的值·。
  • initialBytesToStrip 解码后需要跳过的初始字节数,也就是消息内容字段的起始位置。
  • lengthFieldEndOffset 长度字段结束的偏移量,lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength

对于长度域解码器来说,只要配置好5个特有属性,基本可以解决通信过程中大部分的报文处理。

应用示例

   b.group(bossGroup, workerGroup)
   .channel(NioServerSocketChannel.class)
   .childHandler(new ChannelInitializer() {
	   protected void initChannel(Channel ch) throws Exception {
		   ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(100, 0, 2, 0, 2, true))
				   .addLast(new EchoServerHandler())
	   }
	})

应用场景

场景 1:基于消息长度 + 消息内容的解码

报文只包含消息长度 Length 和消息内容 Content 字段,其中 Length 为 16 进制表示,共占用 2 字节,Length 的值 0x000C 代表 Content 占用 12 字节

BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+

该协议对应的解码器参数组合如下:

lengthFieldOffset = 0,Length 字段就在报文的开始位置。
lengthFieldLength = 2,Length 字段占用 2 字节。
lengthAdjustment = 0,Length 字段只包含消息长度,不需要做任何修正。
initialBytesToStrip = 0,解码后内容依然是 Length + Content,不需要跳过任何初始字节。

场景2:解码结果需要截断

示例 2 与示例 1 的区别在于,解码后的结果只包含消息内容。

BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)
+--------+----------------+      +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
+--------+----------------+      +----------------+

该协议对应的解码器参数组合如下:

lengthFieldOffset = 0,Length 字段就在报文的开始位置。
lengthFieldLength = 2,Length 字段占用 2 字节。
lengthAdjustment = 0,Length 字段只包含消息长度,不需要做任何修正。
initialBytesToStrip = 2,跳过 Length 字段的字节长度,解码后 ByteBuf 中只包含 Content字段。

场景3:长度字段包含消息长度和消息内容所占的字节

Length 字段包含 Length 字段自身的固定长度(2 字节)以及 Content 字段(12 字节)所占用的字节数,Length 的值为 0x000E(2 + 12 = 14 字节),在 Length 字段值的基础上做 lengthAdjustment(-2)的修正,才能得到真实的 Content 字段长度。

BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
+--------+----------------+      +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
+--------+----------------+      +--------+----------------+

对应的解码器参数组合如下:

lengthFieldOffset = 0,因为 Length 字段就在报文的开始位置。
lengthFieldLength = 2,Length 字段占用 2 字节。
lengthAdjustment = -2,长度字段为 14 字节,需要减 2 才是拆包所需要的长度。
initialBytesToStrip = 0,解码后内容依然是 Length + Content,不需要跳过任何初始字节。

场景 4:基于长度字段偏移的解码

报文增加了魔数字段,Length 字段不再是报文的起始位置,Length 字段的值为 0x00000C(长度为 3 字节),表示 Content 字段占用 12 字节,

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" |
+----------+----------+----------------+      +----------+----------+----------------+

该协议对应的解码器参数组合如下:

lengthFieldOffset = 2,Length 字段需要跳过 Header 1 所占用的 2 字节。
lengthFieldLength = 3,Length 字段值为 0x00000C,占用 3字节。
lengthAdjustment = 0, Length 字段只包含消息长度,不需要做任何修正。
initialBytesToStrip = 0,解码后内容依然是完整的报文,不需要跳过任何字节。

场景 5:长度字段与内容字段不再相邻

Length 字段之后是 Header 1, 与 Content 字段不相邻。Length 的值为 0x00000C (12),不包含 header 1 字段,所有也需要修正才能得到 Header + Content 的内容。

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,因为 Length 字段就在报文的开始位置。
lengthFieldLength = 3,Length 字段值为 0x00000C,占用 3字节。
lengthAdjustment = 2, Length 字段(12) + lengthAdjustment 修正字段(2) = Header(2 字节) + Content 的内容(12 字节)。
initialBytesToStrip = 0,解码后内容依然是完整的报文,不需要跳过任何字节。

场景 6:基于长度偏移和长度修正的解码

Length 字段前后分为别 HDR1 和 HDR2 字段,各占用 1 字节,所以既需要做长度字段的偏移,也需要做 lengthAdjustment 修正。

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, 需要跳过 HDR1 所占用的 1 字节,才是 Length 的起始位置。
lengthFieldLength = 2,Length 字段值为 0x000C,占用 2 字节。
lengthAdjustment = 1, Length 字段(12) + lengthAdjustment 字段(1) = HDR2(1 字节) + Content 的内容(12 字节)。
initialBytesToStrip = 3,解码后跳过 HDR1 (1 字节)和 Length 字段(2 字节),共占用 3 字节。

场景 7:长度字段包含除 Content 外的多个其他字段

Length 字段记录了整个报文的长度,包含 Length 自身所占字节、HDR1 、HDR2 以及 Content 字段的长度,解码器需要知道如何进行 lengthAdjustment 调整,才能得到 HDR2 和 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" |
+------+--------+------+----------------+      +------+----------------+

该协议对应的解码器参数组合如下:

lengthFieldOffset = 1, 需要跳过 HDR1 所占用的 1 字节,才是 Length 的起始位置。
lengthFieldLength = 2,Length 字段值为 0x0010,占用 2 字节。
lengthAdjustment = -3, Length 字段(16) + lengthAdjustment 字段(- 3) = HDR2(1 字节) + Content 的内容(12 字节)。
initialBytesToStrip = 3,解码后跳过 HDR1 (1 字节)和 Length 字段(2 字节),共占用 3 字节。

0
广告 广告

评论区