netty基础教程-5、拆包黏包LengthFieldBasedFrameDecoder和LengthFieldPrepender解析

时间:2019-02-11作者:klpeng分类:IT综合浏览:13098评论:0

一、拆包黏包问题

数据在网络中都是以流的形式进行传输的,即像水流一样。没有所谓的开始位置和结束位置。这时就需要我们自己定义协议将数据进行分段处理,否则会出现拆包黏包的问题(数据读到一半解析失败);好在netty为我们提供了一些列的方法;

TCP以流的方式进行数据传输,上层应用协议为了对消息进行区分,往往采用如下4种方式。

  1. 消息长度固定:累计读取到固定长度为LENGTH之后就认为读取到了一个完整的消息。然后将计数器复位,重新开始读下一个数据报文。
  2. 回车换行符作为消息结束符:在文本协议中应用比较广泛。
  3. 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符。
  4. 通过在消息头中定义长度字段来标示消息的总长度。

netty中针对这四种场景均有对应的解码器作为解决方案,比如:

  1. 通过FixedLengthFrameDecoder 定长解码器来解决定长消息的黏包问题;
  2. 通过LineBasedFrameDecoder和StringDecoder来解决以回车换行符作为消息结束符的TCP黏包的问题;
  3. 通过DelimiterBasedFrameDecoder 特殊分隔符解码器来解决以特殊符号作为消息结束符的TCP黏包问题;
  4. 通过LengthFieldBasedFrameDecoder 自定义长度解码器解决TCP黏包问题。

前三种基本上都比较好理解,第一种:指定了消息的长度,第二、三种基本上就是通过特殊符号进行划分消息;重点是第四种LengthFieldBasedFrameDecoder 自定义解析协议,下面就来详细的说一说它。

二、LengthFieldBasedFrameDecoder概述

netty在处理拆包黏包问题时,可以简单的认为是将消息数据分为两段,即:Head(有些协议中没有)、Length、Content

  1. Head:包含了一些消息的元数据,例如:类型,版本等等,
  2. Length:表示数据的长度(解决拆包黏包的关键)
  3. Content:正真的消息内容(一般都将消息内容放在content中)

LengthFieldBasedFrameDecoder在解析消息时就是通过Length来处理消息数据的拆包黏包问题的

举个例子:

+--------+----------------+
| Length | Actual Content |
| 0x000C | "HELLO, WORLD" |
+--------+----------------+

Head没有,只有Length和Content

1、重要参数说明

LengthFieldBasedFrameDecoder的构造方法

public LengthFieldBasedFrameDecoder(
            ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast){...} 
  1. byteOrder:ByteOrder定义了写入buffer时字节的顺序
  2. maxFrameLength:框架的最大长度。, 如果帧的长度大于此值,则将抛出TooLongFrameException。
  3. lengthFieldOffset:长度字段的偏移量:即对应的长度字段在整个消息数据中得位置
  4. lengthFieldLength:长度字段的长度:例如:长度字段是int型表示的,那么这个值就是4(long型就是8)
  5. lengthAdjustment:要添加到长度字段值的补偿值
  6. initialBytesToStrip:从解码帧中去除的第一个字节数
  7. failFast:如果true,TooLongFrameException将被抛出,因为解码器注意到帧的长度将超过 maxFrameLength,无论整个帧是否已经读完。如果 false,则在读取超过maxFrameLength 的整个帧后抛出TooLongFrameException。

读取数据时:先跳到lengthFieldOffset位置,读取长度域lengthFieldLength长度的字节,获取到整个消息数据的长度L;然后继续向后读取(L+lengthAdjustment) 长度的字节,
netty基础教程-5、拆包黏包LengthFieldBasedFrameDecoder和LengthFieldPrepender解析
最后将整个数据从开头丢弃initialBytesToStrip长度的字节,即得到最终的消息数据

注: 0x000c==>12 ; 0x000e==>14

2、具体的使用场景

(1)场景一:

消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段

参数定义:
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)

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

读取了Length值后,再向后读取0x000C(12)个字节(即Content)

(2)场景二:

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

参数定义:
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)

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

解码后的字节缓冲区丢弃了长度字段(2个字节),仅仅包含消息体,对于大多数的协议,解码之后消息长度没有用处,因此可以丢弃。

(3)场景三:

在大多数情况下,length字段仅表示消息正文的长度,如前面的示例所示。 但是,在某些协议中, length字段表示整个消息的长度,包括消息头。在这种情况下,我们指定一个非零 lengthAdjustment 。 由于此示例消息中的长度值始终大于 2 ,因此我们将 -2 指定为lengthAdjustment 进行补偿。

参数定义:
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = -2 (= the length of the Length field)
initialBytesToStrip = 0

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

与场景1不同的是:场景3中长度域的值为14(0x000E)

读取了Length值后,再向后读取0x000C(14+(-2)个字节)个字节(即Content)

(4)场景四:

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

参数定义:
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0

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

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

(5)场景五:

这是一个对场景4的高级版即使用lengthAdjustment对读取的消息长度进行补偿

参数定义:
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
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" |
+----------+----------+----------------+      +----------+----------+----------------+

这里的Length长度为12,Header 1的长度为2,所以使用lengthAdjustment将读取的长度调整为12+2=14即可将Header 1一并读入

(6)场景六:

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

参数定义:
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)

BEFORE DECODE (16 bytes)                       AFTER DECODE (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+
(7)场景七:

这个应该时最复杂的场景了,让我们再举一个例子。 与前一个示例的唯一区别在于,length字段表示整个消息的长度而不是消息正文,就像第三个示例一样。 我们必须将HDR1和Length的长度计入lengthAdjustment 。 请注意,我们不需要考虑HDR2的长度因为长度字段已包含。

参数定义:
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = -3 (= the length of HDR1 + LEN, negative)
initialBytesToStrip = 3

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

长度:
HDR1=1
Length=2
HDR2=1
Content =12
可以看出整个消息的长度为1+2+1+12=16;

解析过程:

  1. 定位到index为1(lengthFieldOffset)的位置
  2. 读取2(lengthFieldLength)个字节,值为16
  3. 继续向后读取16+(-3)=13个字节数据,(-3就是lengthAdjustment)
  4. 最后舍弃开头的3(initialBytesToStrip)个字节,得到最终的解码数据

三、 LengthFieldBasedFrameDecoder源码解析

下面我们就来看看基于消息长度的半包解码器,首先看看入口方法:

protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}

内部调用decode(ChannelHandlerContext ctx, ByteBuf in) 如果解码成功,就将其加入到输出的List out列表中。该函数较长我们还是分几部分来分析:
(1)判断discardingTooLongFrame标识,看是否需要丢弃当前可读的字节缓冲区,如果为真,则执行求其操作。

if (discardingTooLongFrame) {
    //获取需要丢弃的长度
    long bytesToDiscard = this.bytesToDiscard;
    //丢弃的长度不能超过当前缓冲区可读的字节数
    int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
    //跳过需要忽略的字节长度
    in.skipBytes(localBytesToDiscard);
    //bytesToDiscard减去已经忽略的字节长度
    bytesToDiscard -= localBytesToDiscard;
    this.bytesToDiscard = bytesToDiscard;
    failIfNecessary(false);
}

(2)对当前缓冲区中可读字节数和长度偏移量进行对比,如果小于偏移量,谁明缓冲区数据报不够,直接返回null.

//数据报内数据不够,返回null,由IO线程继续读取数据。
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}

int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

if (frameLength < 0) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "negative pre-adjustment length field: " + frameLength);
}

frameLength += lengthAdjustment + lengthFieldEndOffset;

if (frameLength < lengthFieldEndOffset) {
    in.skipBytes(lengthFieldEndOffset);
    throw new CorruptedFrameException(
            "Adjusted frame length (" + frameLength + ") is less " +
            "than lengthFieldEndOffset: " + lengthFieldEndOffset);
}

再后面的具体代码就不分析了,其实核心就是:对消息进行解码,解码之后将解码后的字节数据放到一个新的ByteBuf中返回,并更新原来的消息msg对象的读写索引值。

四、LengthFieldPrepender(编码)功能说明

public LengthFieldPrepender(
            ByteOrder byteOrder, int lengthFieldLength,
            int lengthAdjustment, boolean lengthIncludesLengthFieldLength) {...}
  1. byteOrder:ByteOrder定义了写入buffer时字节的顺序
  2. lengthFieldLength:前置长度字段的长度。 仅允许1,2,3,4和8
  3. lengthAdjustment:要添加到长度字段的值的补偿值
  4. lengthIncludesLengthFieldLength:为true时,length字段的值=length字段的长度+Content的长度。为false时,length字段的值=Content的长度。

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

Before Encode           After Encode
+----------------+      +--------+----------------+
| "HELLO, WORLD" |----->| 0x000C | "HELLO, WORLD" |
+----------------+      +--------+----------------+

通过LengthFieldPrepender可以将待发送消息的长度写入到ByteBuf的前2个字节,编码后的消息组成为长度字段Length+原消息Content的方式。Length值为12,不包括Length自身

通过设置LengthFieldPrepender为true,消息长度将包含长度字段占用的字节数即

Before Encode           After Encode
+----------------+      +--------+----------------+
| "HELLO, WORLD" |----->| 0x000E | "HELLO, WORLD" |
+----------------+      +--------+----------------+

五、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。

protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
    int length = msg.readableBytes() + lengthAdjustment;
    if (lengthIncludesLengthFieldLength) {
        length += lengthFieldLength;
    }

    if (length < 0) {
        throw new IllegalArgumentException(
                "Adjusted frame length (" + length + ") is less than zero");
    }

    switch (lengthFieldLength) {
    case 1:
        if (length >= 256) {
            throw new IllegalArgumentException(
                    "length does not fit into a byte: " + length);
        }
        out.add(ctx.alloc().buffer(1).order(byteOrder).writeByte((byte) length));
        break;
    case 2:
        if (length >= 65536) {
            throw new IllegalArgumentException(
                    "length does not fit into a short integer: " + length);
        }
        out.add(ctx.alloc().buffer(2).order(byteOrder).writeShort((short) length));
        break;
    case 3:
        if (length >= 16777216) {
            throw new IllegalArgumentException(
                    "length does not fit into a medium integer: " + length);
        }
        out.add(ctx.alloc().buffer(3).order(byteOrder).writeMedium(length));
        break;
    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));
        break;
    default:
        throw new Error("should not reach here");
    }
    out.add(msg.retain());
}
打赏
文章版权声明:除非注明,否则均为彭超的博客原创文章,转载或复制请以超链接形式并注明出处。
相关推荐

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

猜你喜欢