写点什么

netty 系列之:netty 中的 frame 解码器

作者:程序那些事
  • 2022 年 4 月 12 日
  • 本文字数:3074 字

    阅读完需:约 10 分钟

netty系列之:netty中的frame解码器

简介

netty 中的数据是通过 ByteBuf 来进行传输的,一个 ByteBuf 中可能包含多个有意义的数据,这些数据可以被称作 frame,也就是说一个 ByteBuf 中可以包含多个 Frame。


对于消息的接收方来说,接收到了 ByteBuf,还需要从 ByteBuf 中解析出有用而数据,那就需要将 ByteBuf 中的 frame 进行拆分和解析。


一般来说不同的 frame 之间会有有些特定的分隔符,我们可以通过这些分隔符来区分 frame,从而实现对数据的解析。


netty 为我们提供了一些合适的 frame 解码器,通过使用这些 frame 解码器可以有效的简化我们的工作。下图是 netty 中常见的几个 frame 解码器:



接下来我们来详细介绍一下上面几个 frame 解码器的使用。

LineBasedFrameDecoder

LineBasedFrameDecoder 从名字上看就是按行来进行 frame 的区分。根据操作系统的不同,换行可以有两种换行符,分别是 "\n" 和 "\r\n" 。


LineBasedFrameDecoder 的基本原理就是从 ByteBuf 中读取对应的字符来和"\n" 跟 "\r\n",可以了可以准确的进行字符的比较,这些 frameDecoder 对字符的编码也会有一定的要求,一般来说是需要 UTF-8 编码。因为在这样的编码中,"\n"和"\r"是以一个 byte 出现的,并且不会用在其他的组合编码中,所以用"\n"和"\r"来进行判断是非常安全的。


LineBasedFrameDecoder 中有几个比较重要的属性,一个是 maxLength 的属性,用来检测接收到的消息长度,如果超出了长度限制,则会抛出 TooLongFrameException 异常。


还有一个 stripDelimiter 属性,用来判断是否需要将 delimiter 过滤掉。


还有一个是 failFast,如果该值为 true,那么不管 frame 是否读取完成,只要 frame 的长度超出了 maxFrameLength,就会抛出 TooLongFrameException。如果该值为 false,那么 TooLongFrameException 会在整个 frame 完全读取之后再抛出。


LineBasedFrameDecoder 的核心逻辑是先找到行的分隔符的位置,然后根据这个位置读取到对应的 frame 信息,这里来看一下找到行分隔符的 findEndOfLine 方法:


    private int findEndOfLine(final ByteBuf buffer) {        int totalLength = buffer.readableBytes();        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);        if (i >= 0) {            offset = 0;            if (i > 0 && buffer.getByte(i - 1) == '\r') {                i--;            }        } else {            offset = totalLength;        }        return i;    }
复制代码


这里使用了一个 ByteBuf 的 forEachByte 对 ByteBuf 进行遍历。我们要找的字符是:ByteProcessor.FIND_LF。


最后 LineBasedFrameDecoder 解码之后的对象还是一个 ByteBuf。

DelimiterBasedFrameDecoder

上面讲的 LineBasedFrameDecoder 只对行分隔符有效,如果我们的 frame 是以其他的分隔符来分割的话 LineBasedFrameDecoder 就用不了了,所以 netty 提供了一个更加通用的 DelimiterBasedFrameDecoder,这个 frameDecoder 可以自定义 delimiter:


public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) { this(maxFrameLength, true, delimiter); }
复制代码


传入的 delimiter 是一个 ByteBuf,所以 delimiter 可能不止一个字符。


为了解决这个问题在 DelimiterBasedFrameDecoder 中定义了一个 ByteBuf 的数组:


    private final ByteBuf[] delimiters;
delimiters= delimiter.readableBytes();
复制代码


这个 delimiters 是通过调用 delimiter 的 readableBytes 得到的。


DelimiterBasedFrameDecoder 的逻辑和 LineBasedFrameDecoder 差不多,都是通过对比 bufer 中的字符来对 bufer 中的数据进行截取,但是 DelimiterBasedFrameDecoder 可以接受多个 delimiters,所以它的用处会根据广泛。

FixedLengthFrameDecoder

除了进行 ByteBuf 中字符比较来进行 frame 拆分之外,还有一些其他常见的 frame 拆分的方法,比如根据特定的长度来区分,netty 提供了一种这样的 decoder 叫做 FixedLengthFrameDecoder。


public class FixedLengthFrameDecoder extends ByteToMessageDecoder 
复制代码


FixedLengthFrameDecoder 也是继承自 ByteToMessageDecoder,它的定义很简单,可以传入一个 frame 的长度:


    public FixedLengthFrameDecoder(int frameLength) {        checkPositive(frameLength, "frameLength");        this.frameLength = frameLength;    }
复制代码


然后调用 ByteBuf 的 readRetainedSlice 方法来读取固定长度的数据:


in.readRetainedSlice(frameLength)
复制代码


最后将读取到的数据返回。

LengthFieldBasedFrameDecoder

还有一些 frame 中包含了特定的长度字段,这个长度字段表示 ByteBuf 中有多少可读的数据,这样的 frame 叫做 LengthFieldBasedFrame。


netty 中也提供了一个对应的处理 decoder:


public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder 
复制代码


读取的逻辑很简单,首先读取长度,然后再根据长度再读取数据。为了实现这个逻辑,LengthFieldBasedFrameDecoder 提供了 4 个字段,分别是 lengthFieldOffset,lengthFieldLength,lengthAdjustment 和 initialBytesToStrip。


lengthFieldOffset 指定了长度字段的开始位置,lengthFieldLength 定义的是长度字段的长度,lengthAdjustment 是对 lengthFieldLength 进行调整,initialBytesToStrip 表示是否需要去掉长度字段。


听起来好像不太好理解,我们举几个例子,首先是最简单的:


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


要编码的消息有个长度字段,长度字段后面就是真实的数据,0x000C 是一个十六进制,表示的数据是 12,也就是"HELLO, WORLD" 中字符串的长度。


这里 4 个属性的值是:


   lengthFieldOffset   = 0   lengthFieldLength   = 2   lengthAdjustment    = 0   initialBytesToStrip = 0 
复制代码


表示的是长度字段从 0 开始,并且长度字段占有两个字节,长度不需要调整,也不需要对字段进行调整。


再来看一个比较复杂的例子,在这个例子中 4 个属性值如下:


   lengthFieldOffset   = 1     lengthFieldLength   = 2   lengthAdjustment    = 1     initialBytesToStrip = 3  
复制代码


对应的编码数据如下所示:


BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)   +------+--------+------+----------------+      +------+----------------+   | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |   | 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |   +------+--------+------+----------------+      +------+----------------+
复制代码


上面的例子中长度字段是从第 1 个字节开始的(第 0 个字节是 HDR1),长度字段占有 2 个字节,长度再调整一个字节,最终数据的开始位置就是 1+2+1=4,然后再截取前 3 个字节的数据,得到了最后的结果。

总结

netty 提供的这几个基于字符集的 frame decoder 基本上能够满足我们日常的工作需求了。当然,如果你传输的是一些更加复杂的对象,那么可以考虑自定义编码和解码器。自定义的逻辑步骤和上面我们讲解的保持一致就行了。


本文已收录于 http://www.flydean.com/14-5-netty-frame-decoder/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 21 小时前阅读数: 15
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:netty中的frame解码器_Netty_程序那些事_InfoQ写作平台