自顶向下深入分析Netty(八)-- LengthFieldBasedFrameDecoder

(本文是上一节的节选,已读可略过)
Netty中,基于长度字段的消息帧解码器LengthFieldBasedFrameDecoder可根据数据包中的长度字段动态的解码出消息帧。一个推荐的二进制传输协议可设计为如下格式:

    +----------+------+----------+------+
    |  头部长度 |  头部 |  数据长度 | 数据 |
    +----------+------+----------+------+

这样的协议可满足大多数场景使用,但不幸的是:很多情况下并不可以设计新的协议,往往要在老旧的协议上传输数据。由此,Netty将该解码器设计的十分通用,只要有类似的长度字段便能正确解码出消息帧。当然前提是:正确使用解码器。
没有什么是完美的,由于该解码器十分通用,所以有大量的配置变量:

    private final ByteOrder byteOrder;
    private final int maxFrameLength;
    private final boolean failFast;
    private final int lengthFieldOffset;
    private final int lengthFieldLength;
    private final int lengthAdjustment;
    private final int initialBytesToStrip;

变量byteOrder表示长度字段的字节序:大端或小端,默认为大端。如果对字节序有疑问,请查阅其他资料,不再赘述。maxFrameLengthfailFast与其他解码器相同,控制最大帧长度和快速失败抛异常,注意:该解码器failFast默认为true。
接下来将重点介绍其它四个变量:

  1. lengthFieldOffset表示长度字段偏移量即在一个数据包中长度字段的具体下标位置。标准情况,该长度字段为数据部分长度。
  2. lengthFieldLength表示长度字段的具体字节数,如一个int占4字节。该解码器支持的字节数有:1,2,3,4和8,其他则会抛出异常。另外,还需要注意的是:长度字段的结果为无符号数
  3. lengthAdjustment是一个长度调节量,当数据包的长度字段不是数据部分长度而是总长度时,可将此值设定为头部长度,便能正确解码出包含整个数据包的结果消息帧。注意:某些情况下,该值可设定为负数。
  4. initialBytesToStrip表示需要略过的字节数,如果我们只关心数据部分而不关心头部,可将此值设定为头部长度从而丢弃头部。
    下面我们使用具体的例子来说明:
  • 需求1:如下待解码数据包,正确解码为消息帧,其中长度字段在最前面的2字节,数据部分为12字节的字符串"HELLO, WORLD",长度字段0x000C=12 表示数据部分长度,数据包总长度则为14字节。
      解码前(14 bytes)                 解码后(14 bytes)
     +--------+----------------+      +--------+----------------+
     | Length | Actual Content |----->| Length | Actual Content |
     | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
     +--------+----------------+      +--------+----------------+

正确配置(只列出四个值中不为0的值):

    lengthFieldLength = 2;
  • 需求2:需求1的数据包不变,消息帧中去除长度字段。
     解码前(14 bytes)                 解码后(12 bytes)
    +--------+----------------+      +----------------+
    | Length | Actual Content |----->| Actual Content |
    | 0x000C | "HELLO, WORLD" |      | "HELLO, WORLD" |
    +--------+----------------+      +----------------+

正确配置:

    lengthFieldLength   = 2;
    initialBytesToStrip = 2;
  • 需求3:需求1数据包中长度字段表示数据包总长度。
     解码前(14 bytes)                 解码后(14 bytes)
    +--------+----------------+      +--------+----------------+
    | Length | Actual Content |----->| Length | Actual Content |
    | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
    +--------+----------------+      +--------+----------------+

正确配置:

    lengthFieldLength =  2;
    lengthAdjustment  = -2;  // 调整长度字段的2字节
  • 需求4:综合难度,数据包有两个头部HDR1和HDR2,长度字段以及数据部分组成,其中长度字段值表示数据包总长度。结果消息帧需要第二个头部HDR2和数据部分。请先给出答案再与标准答案比较,结果正确说明你已完全掌握了该解码器的使用。
解码前 (16 bytes)                               解码后 (13 bytes)
+------+--------+------+----------------+      +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" |      | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+      +------+----------------+

正确配置:

    lengthFieldOffset   =  1;
    lengthFieldLength   =  2;
    lengthAdjustment    = -3;
    initialBytesToStrip =  3;

本解码器的解码过程总体上较为复杂,由于解码的代码是在while循环里面,decode方法return或者抛出异常时可看做一次循环结束,直到in中数据被解析完或者in的readerIndex读索引不再增加才会从while循环跳出。使用状态的思路理解,每个return或者抛出异常看为一个状态:

  1. 状态1:丢弃过长帧状态,可能是用户设置了错误的帧长度或者实际帧过长。
    if (discardingTooLongFrame) {
        long bytesToDiscard = this.bytesToDiscard;
        int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
        in.skipBytes(localBytesToDiscard); // 丢弃实际的字节数
        
        bytesToDiscard -= localBytesToDiscard;
        this.bytesToDiscard = bytesToDiscard;
        failIfNecessary(false);
    }

变量localBytesToDiscard取得实际需要丢弃的字节数,由于过长帧有两种情况:a.用户设置了错误的长度字段,此时in中并没有如此多的字节;b.in中确实有如此长度的帧,这个帧确实超过了设定的最大长度。bytesToDiscard的计算是为了failIfNecessary()确定异常的抛出,其值为0表示当次丢弃状态已经丢弃了in中的所有数据,可以对新读入in的数据进行处理;否则,还处于异常状态。

    private void failIfNecessary(boolean firstDetectionOfTooLongFrame) {
        if (bytesToDiscard == 0) {
            long tooLongFrameLength = this.tooLongFrameLength;
            this.tooLongFrameLength = 0;
            // 由于已经丢弃所有数据,关闭丢弃模式
            discardingTooLongFrame = false;
            // 已经丢弃了所有字节,当非快速失败模式抛异常
            if (!failFast || firstDetectionOfTooLongFrame) {
                fail(tooLongFrameLength);
            }
        } else {
            if (failFast && firstDetectionOfTooLongFrame) {
                // 帧长度异常,快速失败模式检测到即抛异常
                fail(tooLongFrameLength);
            }
        }
    }

可见,首次检测到帧长度是一种特殊情况,在之后的一个状态进行分析。请注意该状态并不是都抛异常,还有可能进入状态2。

  1. 状态2:in中数据不足够组成消息帧,此时直接返回null等待更多数据到达。
    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }
  1. 状态3:帧长度错误检测,检测长度字段为负值得帧以及加入调整长度后总长小于长度字段的帧,均抛出异常。
    int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
    // 该方法取出长度字段的值,不再深入分析
    long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, 
                             lengthFieldLength, byteOrder);
    if (frameLength < 0) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException("...");
    }

    frameLength += lengthAdjustment + lengthFieldEndOffset;
    if (frameLength < lengthFieldEndOffset) {
        in.skipBytes(lengthFieldEndOffset);
        throw new CorruptedFrameException("...");
    }
  1. 状态4:帧过长,由前述可知:可能是用户设置了错误的帧长度或者实际帧过长。
    if (frameLength > maxFrameLength) {
            long discard = frameLength - in.readableBytes();
            tooLongFrameLength = frameLength;

            if (discard < 0) {
                in.skipBytes((int) frameLength);
            } else {
                discardingTooLongFrame = true;
                bytesToDiscard = discard;
                in.skipBytes(in.readableBytes());
            }
            failIfNecessary(true);
            return null;
        }

变量discard<0表示当前收到的数据足以确定是实际的帧过长,所以直接丢弃过长的帧长度;>0表示当前in中的数据并不足以确定是用户设置了错误的帧长度,还是正确帧的后续数据字节还没有到达,但无论何种情况,将丢弃状态discardingTooLongFrame标记设置为true,之后后续数据字节进入状态1处理。==0时,在failIfNecessary(true)无论如何都将抛出异常,><0时,只有设置快速失败才会抛出异常。还需注意一点:failIfNecessary()的参数firstDetectionOfTooLongFrame的首次是指正确解析数据后发生的第一次发生的帧过长,可知会有很多首次。

  1. 状态5:正确解码出消息帧。
    int frameLengthInt = (int) frameLength;
    if (in.readableBytes() < frameLengthInt) {
        return null;    // 到达的数据还达不到帧长
    }

    if (initialBytesToStrip > frameLengthInt) {
        in.skipBytes(frameLengthInt);   // 跳过字节数错误
        throw new CorruptedFrameException("...");
    }
    in.skipBytes(initialBytesToStrip);

    // 正确解码出数据帧
    int readerIndex = in.readerIndex();
    int actualFrameLength = frameLengthInt - initialBytesToStrip;
    ByteBuf frame = in.slice(readerIndex, actualFrameLength).retain();
    in.readerIndex(readerIndex + actualFrameLength);
    return frame;

代码中混合了两个简单状态,到达的数据还达不到帧长和用户设置的忽略字节数错误。由于较为简单,故合并到一起。
至此解码框架分析完毕。可见,要正确的写出基于长度字段的解码器还是较为复杂的,如果开发时确有需求,特别要注意状态的转移。下面介绍较为简单的编码框架。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 编解码处理器作为Netty编程时必备的ChannelHandler,每个应用都必不可少。Netty作为网络应用框架...
    Hypercube阅读 3,639评论 7 12
  • 简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者...
    保川阅读 5,941评论 1 13
  • 实时消息协议---流的分块 版权声明: 版权(c)2009 Adobe系统有限公司。全权所有。 摘要: 本备忘录描...
    一个人zy阅读 1,887评论 0 9
  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,654评论 0 3
  • 为什么会迷上编绳,我想可能源自于那个绳结平凡又简单的力量吧。 小时候看大人手里拿着一根棉线,几段彩色的线不出一会儿...
    悠爷阅读 1,301评论 2 1