03.Netty进阶之长度域解码器

03.Netty进阶之长度域解码器

一、概述

长度域解码器(LengthFieldBasedFrameDecoder)是解决 TCP 拆包/粘包问题最常用的解码器

它基本上可以覆盖大部分基于长度拆包场景,开源消息中间件 RocketMQ 就是使用长度域解码器进行解码的。

长度域解码器相比固定长度解码器特殊分隔符解码器要复杂一些,接下来我们就一起学习下这个强大的解码器。

二、重要属性

  • 长度域解码器特有属性
// 长度字段的偏移量,也就是存放长度数据的起始位置
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;
  • 与固定长度解码器和特定分隔符解码器相似的属性
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进行测试

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;

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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值