摘要:于以消息长度标识的包协议,首先要知道消息的长度字段,再通过长度字段来读取相应长度的字节,这样才能获得完整的包消息。
使用TCP协议的应用中,读取消息时会发生粘包和拆包的问题(参考《TCP粘包与拆包分析及解决之道》),所以我们需要在自己的应用中处理粘包和拆包,解决粘包和拆包的方法有:
  1. 消息定长,如固定协议包长度是100字节,如果不够,包尾空格补位;
  2. 包尾增加换行符或者其它符号进行分割,如FTP协议;
  3. 将消息分为消息头和消息体,在消息头固定的位置增加一个字段,表示消息的总长度(或消息的长度),参照TCP协议;
  4. 使用更复杂的应用层协议;

LengthFieldBasedFrameDecoder是Netty提供的一种解码器,它定义了一个长度的字段来表示消息的长度,因此能够处理可变长度的消息。显然该解码器解决粘包和拆包的方法对于上面的第3点,将消息分为消息头和消息体,消息头固定位置增加一个表示长度的字段,通过长度字段来获取整包的信息。继承关系如下所示:
可以看到 LengthFieldBasedFrameDecoder继承了ByteToMessageDecoder,这样对于转换字节为POJO对象的底层工作就交给ByteToMessageDecoder类来实现了, LengthFieldBasedFrameDecoder类只需要负责对消息的字节流进行解包即可。

LengthFieldBasedFrameDecoder类定义了几个构造函数,其实这些构造函数大多是为了简化使用难度而定义的,它们提供了一些默认的参数,最终调用的构造函数如下所示:

LengthFieldBasedFrameDecoder(
    ByteOrder byteOrder,
    int maxFrameLength,
    int lengthFieldOffset,
    int lengthFieldLength,
    int lengthAdjustment,
    int initialBytesToStrip,
    boolean failFast)
构造函数里面的所有参数都有一个对应的属性,分别为:
  • byteOrder:网络字节序,默认为大端字节序;
  • maxFrameLength:数据帧的最大长度;
  • lengthFieldOffset:消息中长度字段偏移的字节数;
  • lengthFieldLength:消息中长度字段占用的字节数;
  • lengthAdjustment:该字段加长度字段等于数据帧的长度;
  • initialBytesToStrip:从数据帧中跳过的字节数;
通过控制上面的这些参数,我们就可以从接收到的字节流中解出我们需要的协议包消息,并且按照我们想要的协议消息的某一段返回,如不希望包消息中包含有长度字段,可以控制initialBytesToStrip来跳过。

可能你还不理解上面那些属性的真正意义,没关系,后面解析源码的时候会让你明白这些属性的作用的。我们再来看一下重写的decoder()方法,代码如下所示:
@Override
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中调用了decode(ChannelHandlerContext ctx, ByteBuf buffer) 来处理解包逻辑,下面根据decode(ChannelHandlerContext ctx, ByteBuf buffer) 的源码详细解析LengthFieldBasedFrameDecoder实现解包的原理。

开始之前,我们稍微理一下思路,对于像流水一样的消息流,怎样得到一个完整的包消息,即怎么知道某个包在哪里结束呢?对于以换行符为结束的包协议,很明显读取到有换行字符就认为一个包结束了;对于以消息长度标识的包协议,首先要知道消息的长度字段,再通过长度字段来读取相应长度的字节,这样才能获得完整的包消息。所以,关键是要获取消息的长度字段,进而获得消息的真实长度。

看下源码,如下所示:
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}
lengthFieldEndOffset在构造函数中被初始化,如下所示:
lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;
结合上面这两段代码,可以看到至少需要读取lengthFieldOffset + lengthFieldLength长度的字节,才能包含表示长度的字段。

下面就应该取出表示长度字段的值了,如下所示:
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder);

protected long getUnadjustedFrameLength(ByteBuf buf, int offset, 
      int length, ByteOrder order) {
    buf = buf.order(order);
    long frameLength;
    switch (length) {
    case 1:
        frameLength = buf.getUnsignedByte(offset);
        break;
    case 2:
        frameLength = buf.getUnsignedShort(offset);
        break;
    case 3:
        frameLength = buf.getUnsignedMedium(offset);
        break;
    case 4:
        frameLength = buf.getUnsignedInt(offset);
        break;
    case 8:
        frameLength = buf.getLong(offset);
        break;
    default:
        throw new DecoderException(
                "unsupported lengthFieldLength: " 
         + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)");
    }
    return frameLength;

通过in.readerIndex() + lengthFieldOffset 获取长度字段所在的位置,然后读取消息长度字段所占字节数的字节(消息长度字段占用的字节数是协议约定好的),读取出来的数值就是消息长度的值了。getUnadjustedFrameLength()这个方法就是用来获取消息的长度的,看源码可以发现它对于协议约定的消息长度字段占用的字节数是有要求的,占用的字节数必须是1,2,3,4,8这几个值中的某一个,其它的数值是不合法的。注意,getUnsignedByte()方法不会改变buf的readIndex、writeIndex的状态。

从上面的代码中,我们已经获取了消息的长度值了,假设用LEN表示,正常来说再读取LEN长度的字节就可以获取完整的消息了,但是Netty的这个解码器做得更加强大,它可以让你设置lengthAdjustment这个属性来调整消息长度的值。

为什么要调整消息长度的值呢?
我们来想像一下这样的场景,假设我们的协议是:消息头部1+消息头部2+消息体,其中消息头部1仅仅包含一个消息长度字段,消息长度字段表示消息体的长度,消息头部2固定长度为2,假如我们想获得头部2+消息体的信息呢?显然,用消息长度+头部2占用的字节数即可得到想要的消息的真正长度,即lengFieldLength+lengthAdjustment=真正长度,其中lengthAdjustment=2。

再回来看一下源码,如下所示:
frameLength += lengthAdjustment + lengthFieldEndOffset;
上面的frameLength其实上消息的长度LEN,LEN再加上lengthAdjustment + lengthFieldEndOffset,得到的是整个包的消息长度。

清楚lengthAdjustment这个属性的用法后,我们再看一下initialBytesToStrip属性,它表示从数据帧中跳过的字节数。我们再想像一下这样的场景,假设我们的协议是:消息头部1+消息头部2+消息体,消息头部1长度固定为2个字节,假设现在已经获得整个协议包消息的总长度为20字节,但我们只想要解码后返回消息头部2+消息体的信息,那么就需要在获得整个协议消息后,需要跳过前面的2个字节,截取后面的18个字节返回。

对于上面的场景,Netty的这个解码器使用initialBytesToStrip来设置跳过的字节,即initialBytesToStrip=2。获取消息的总长度后,那么如果设置了initialBytesToStrip的值,那么就应该跳过initialBytesToStrip 长度的字节后,再读取消息块返回,如下所示:
in.skipBytes(initialBytesToStrip);

// extract frame
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
注意,extractFrame方法仅仅是提取字节流出来,而不会改变buf中的readIndex指针,所以需要在提取字节后重新设置readIndex指针,以便解析下一个包消息。


在使用LengthFieldBasedFrameDecoder解包的时候,我们还需要注意判断包的总长度会不会超过我们设定的最大长度,我们看下源码,如下所示:
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;
}
如果总长度frameLength超出了设定的最大长度maxFrameLength,那么我们就认为这个包是不合法的,需要把它丢弃。源码中处理得比较巧妙,它首先比较frameLength与可读字节的大小,如果可读字节比frameLength大,直接丢弃frameLength长度的字节即可把整个不合法的包完全丢弃了,这样再读取下一个包消息即可。如果可读字节比frameLength小,说明即使把可读的字节完全丢弃,这个不合法的包消息还没有完全被丢弃,后续读取到的字节还是存在这个包的字节,所以需要设置discardingTooLongFrame = true,表示后续字节还需要丢弃,丢弃的字节数为discard,最后再把这次可读的所有字节丢弃,因为这次所有可读的字节都是不合法的包的字节。

在下一次读取包消息的时候,需要判断是否存在需要丢弃的包消息,即判断discardingTooLongFrame是否等于true,如下所示:
if (discardingTooLongFrame) {
    long bytesToDiscard = this.bytesToDiscard;
    int localBytesToDiscard = (int) Math.min(bytesToDiscard, in.readableBytes());
    in.skipBytes(localBytesToDiscard);
    bytesToDiscard -= localBytesToDiscard;
    this.bytesToDiscard = bytesToDiscard;

    failIfNecessary(false);
}
这段代码写得非常的巧妙,代码Math.min(bytesToDiscard, in.readableBytes())得到应该丢弃的字节数,丢弃后通过bytesToDiscard -= localBytesToDiscard得到剩余应该丢弃的字节数,如果等于0,那么应该把discardingTooLongFrame设置为false,在failIfNecessary方法中做了这样的判断,如下所示:
if (bytesToDiscard == 0) {
    long tooLongFrameLength = this.tooLongFrameLength;
    this.tooLongFrameLength = 0;
    discardingTooLongFrame = false;
...
如果这一次读取的字节数还不能把所有应该丢弃的字节都丢弃完,那么应该这次读取的所有字节都要被完全丢弃,所以if (in.readableBytes() < lengthFieldEndOffset)为true,那么返回null,如下所示:
if (in.readableBytes() < lengthFieldEndOffset) {
    return null;
}

下一次读取字节继续重复上面的过程,丢弃应该丢弃的字节后,再读取下一个新的协议包的消息。


整个LengthFieldBasedFrameDecoder源码的处理过程就已经解释完了,有些地方可能在文字上没能表达清楚,如果对此有疑问,欢迎各位童鞋加群大家一起讨论:
QQ群:399643539

版权说明:如无特殊说明,文章均为本站原创,如需转载请注明出处

本文标题:Netty5源码之LengthFieldBasedFrameDecoder

本文地址:http://www.wolfbe.com/detail/201610/380.html

本文标签: decoder netty java

感谢您的支持,朗度云将继续前行

扫码打赏,金额随意

温馨提醒:打赏一旦完成,金额无法退还,请谨慎操作!

扫二维码 我要反馈 回到顶部