LengthFieldBasedFrameDecoder

发表时间:2017-10-26 15:49:20 浏览量( 44 ) 留言数( 0 )

学习目标:

1、了解LengthFieldBasedFrameDecoder

2、了解LengthFieldPrepender的编码


学习过程:

一、LengthFieldBasedFrameDecoder

    本节课也属于昨天的内容,因为这个比较重要,也比较有难度一点,所以就拿到今天来讲解了。LengthFieldBasedFrameDecoder通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息,只要传入正确的参数,就可以轻松解决“读半包”的问题。该类的构造方法的参数比较多:

  • maxFrameLength:解码的帧的最大长度。

  • lengthFieldOffset :长度属性的起始位(偏移位),包中存放有整个大数据包长度的字节,这段字节的其实位置。

  • lengthFieldLength:长度属性的长度,即存放整个大数据包长度的字节所占的长度。

  • lengthAdjustmen:长度调节值,在总长被定义为包含包头长度时,修正信息长度。initialBytesToStrip:跳过的字节数,根据需要我们跳过lengthFieldLength个字节,以便接收端直接接受到不含“长度属性”的内容。

  • failFast :为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常。

   你可以点击进去看一下源代码的注释,文档里面的说明非常详细,针对各种情况都有明确的说明了。下面我们就对代码中简单说明一下

(1)例子一:消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段,消息结构定义如下: 

消息结构图


在解码前字节缓冲区占了14个字节,其中前两个字节是标识长度的字节,后面12个字节是消息体。 使用的组合参数如下: 

1) lengthFieldOffset = 0;//长度字段的偏差

2) lengthFieldLength = 2;//长度字段占的字节数

3) lengthAdjustment = 0;//添加到长度字段的补偿值

4) initialBytesToStrip = 0。//从解码帧中第一次去除的字节数

解码后的字节缓冲区的内容是: 解码后还是14个字节。

 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)

 * +--------+----------------+      +--------+----------------+

 * | Length | Actual Content |----->| Length | Actual Content |

 * | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |

 * +--------+----------------+      +--------+----------------+

 * </pre>


(2)例子二:通过ByteBuf.readableBytes()方法我们可以获取当前消息的长度,所以解码后的字节缓冲区可以不携带长度字段,由于长度字段在起始位置并且长度为2,所以将initialBytesToStrip设置为2。参数组合修改为: 

1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 2。

这时候解码后字节缓冲区的数据就是: 

 * BEFORE DECODE (14 bytes)         AFTER DECODE (12 bytes)

 * +--------+----------------+      +----------------+

 * | Length | Actual Content |----->| Actual Content |

 * | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |

 * +--------+----------------+      +----------------+

 * </pre>

很明显跳过了长度字段后的字节缓冲区就只有12个字节了。解码后的字节缓冲区丢弃了长度字段,仅仅包含消息体,对于大多数的协议,解码之后消息长度没有用处,因此可以丢弃。


(3)例子三:在大多数的应用场景中,长度字段仅用来标识消息体的长度,这类协议通常由消息长度字段+消息体组成,如上图所示的几个例子。但是,对于某些协议,长度字段还包含了消息头的长度。在这种应用场景中,往往需要使用lengthAdjustment进行修正。由于整个消息(包含消息头)的长度往往大于消息体的长度,所以,lengthAdjustment为负数。下图展示了通过指定lengthAdjustment字段来包含消息头的长度: 

1) lengthFieldOffset = 0;

2) lengthFieldLength = 2;

3) lengthAdjustment = -2

4) initialBytesToStrip = 0。


 * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)

 * +--------+----------------+      +--------+----------------+

 * | Length | Actual Content |----->| Length | Actual Content |

 * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |

 * +--------+----------------+      +--------+----------------+

 * </pre>


(4)例子四:但是由于协议的种类繁多,并不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用lengthFieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题: 

1) lengthFieldOffset = 2;

2) lengthFieldLength = 3;

3) lengthAdjustment = 0;

4) initialBytesToStrip = 0。


其中lengthFieldOffset表示长度字段在消息头中偏移的字节数,lengthFieldLength 表示长度字段自身的长度,解码效果如下: 

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

 * +----------+----------+----------------+      +----------+----------+----------------+

 * </pre>

    由于消息头1的长度为2,所以长度字段的偏移量为2;消息长度字段Length为3,所以lengthFieldLength值为3。由于长度字段仅仅标识消息体的长度,所以lengthAdjustment和initialBytesToStrip都为0。


(5)场景五:最后一种场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其它消息头字段,在这种场景下如果想忽略长度字段以及其前面的其它消息头字段,则可以通过initialBytesToStrip参数来跳过要忽略的字节长度,它的组合配置示意如下: 

1) lengthFieldOffset = 1;

2) lengthFieldLength = 2;

3) lengthAdjustment = 1;

4) 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" |

 * +------+--------+------+----------------+      +------+----------------+

 * </pre>

   由于HDR1的长度为1,所以长度字段的偏移量lengthFieldOffset为1;长度字段为2个字节,所以lengthFieldLength为2。由于长度字段是消息体的长度,解码后如果携带消息头中的字段,则需要使用lengthAdjustment进行调整,此处它的值为1,代表的是HDR2的长度,最后由于解码后的缓冲区要忽略长度字段和HDR1部分,所以lengthAdjustment为3。解码后的结果为13个字节,HDR1和Length字段被忽略。

还有其他的一些示例

 * lengthFieldOffset   =  1

 * lengthFieldLength   =  2

 * <b>lengthAdjustment</b>    = <b>-3</b> (= the length of HDR1 + LEN, negative)

 * <b>initialBytesToStrip</b> = <b> 3</b>

 *

 * BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)

 * +------+--------+------+----------------+      +------+----------------+

 * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |

 * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |

 * +------+--------+------+----------------+      +------+----------------+

 * </pre>

  从上面的示例可以看到,这个方法非常灵活,在传输是通过计算长度,并设置长度表示,接收方就可以根据这个长度标识进行解析接收。



二、LengthFieldPrepender

    LengthFieldBasedFrameDecoder这个解码器需要计算需要发送的字节长度,并添加到发送的字节码中,当然这对我们来说并没有什么难度,但是Netty已经帮我们实现了一个专门的编码器了。

1. LengthFieldPrepender功能说明

如果协议中的第一个字段为长度字段,netty提供了LengthFieldPrepender编码器,它可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中,如

 * <pre>

 * +----------------+

 * | "HELLO, WORLD" |

 * +----------------+

 * </pre>

 * into the following:

 * <pre>

 * +--------+----------------+

 * + 0x000C | "HELLO, WORLD" |

 * +--------+----------------+

 * </pre>


通过设置lengthIncludesLengthFieldLength为true,消息长度将包含长度字段占用的字节数,打开lengthIncludesLengthFieldLength后,图3-3示例中的编码结果如下图所示: 

 * (12 (original data) + 2 (prepended data) = 14 (0xE)):

 * <pre>

 * +--------+----------------+

 * + 0x000E | "HELLO, WORLD" |

 * +--------+----------------+

 * </pre>

    2. LengthFieldPrepender源码解析

    LengthFieldPrepender工作原理分析如下:首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieldLength的长度。

    如果调整后的消息长度小于0,则抛出参数非法异常。对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:

    1) 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeByte将长度值写入到ByteBuf中;

    2) 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeShort将长度值写入到ByteBuf中;

    3) 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的ByteBuf并通过writeMedium将长度值写入到ByteBuf中;

    4) 长度字段所占字节为4:创建新的ByteBuf,并通过writeInt将长度值写入到ByteBuf中;

    5) 长度字段所占字节为8:创建新的ByteBuf,并通过writeLong将长度值写入到ByteBuf中;

    6) 其它长度值:直接抛出Error。

    可以看一下源代码,也是非常简单的。

    case 4:

        out.add(ctx.alloc().buffer(4).order(byteOrder).writeInt(length));

        break;

    case 8:

        out.add(ctx.alloc().buffer(8).order(byteOrder).writeLong(length));