netty源码学习:谨慎的继承LengthFieldBasedFrameDecoder。

先上结论:你应当优先使用组合的方式使用LengthFieldBasedFrameDecoder,就像这样:(√)

ChannelPipeline pipeline=channel.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(8192,0,4,0,4))
        .addLast(new ByteToProtoBufDecoder())
        .addLast(new ProtoBufToByteEncoder());
public class ByteToProtoBufDecoder extends SimpleChannelInboundHandler<ByteBuf>

你应该在下一个inboundHandler中处理由LengthFieldBasedFrameDecoder为你切割好的帧。

 
 
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
    //消息的前4个字节已被跳过,所以这里已经没有了长度字段,只剩下内容部分

    //编码时使用了一个int标记protoBuf协议的类,那在解码时需要先取出该
    int protoIndex=msg.readInt();

    //通过索引获得该协议对应的解析器(客户端与服务器需要保持索引的一致性)
    Parser parser= ProtoBufEnum.parserOfProtoIndex(protoIndex);
    if (parser==null){
        throw new IllegalArgumentException("illegal protoIndex " + protoIndex);//自己决定如何处理协议无法解析的情况
    }

    try (ByteBufInputStream bufInputStream=new ByteBufInputStream(msg)){
        MessageLite messageLite= (MessageLite) parser.parseFrom(bufInputStream);
        ctx.fireChannelRead(messageLite);//将消息传递下去,或者在这里将消息发布出去
    }
}

(这个示例是一个支持任意个数protoBuf编解码的demo,可参考博客 基于netty实现的支持任意个数protobuf编解码通信 )

而不是继承它,就像这样:(×)

p.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
        new ObjectEchoServerHandler());

public class ObjectDecoder extends LengthFieldBasedFrameDecoder 
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    ByteBuf frame = (ByteBuf) super.decode(ctx, in);
    if (frame == null) {
        return null;
    }

    ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver);
    try {
        return ois.readObject();
    } finally {
        ois.close();
    }
}

//------------------------------------------------------分割线----------------------------------------------------------

    LengthFieldBasedFrameDecoder是netty为我们提供的一个解决拆包、粘包问题的类。在学习LengthFieldBasedFrameDecoder之初,源码看的不是很懂,博主查了一些博客文章,许多文章的示例是继承它。但是在彻底理解LengthFieldBasedFrameDecoder的源码之后,发现这并不是一个适合继承使用的类

LengthFieldBasedFrameDecoder有两个方法可以被子类覆盖,分别为decode(),extractFrame()方法。



说一下继承它的坏处:

1.在你未彻底理解LengthFieldBasedFrameDecoder源码之前,覆盖两个方法中的任意一个方法都极易出错,这两个方法之间有联系,且都可以被覆盖,你要覆盖其中任何一个都必须搞懂这两个方法干了什么。

2.会导致学习与理解难度的增加,你的类对于别人来说不是那么易于使用。

3.覆盖它的方法,决定了你无法写出优雅的代码。

下面从源码入手,说一说为什么继承它不好:

    网络中传输的总是字节流,所以在解码时,我们第一步常常是将字节转换为消息对象,netty为我们提供了抽象类ByteToMessageDecoder,并定义了一个deocde()方法,每次从channel中读取到数据时,都会进行调用:

protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception

LengthFieldBasedFrameDecoder继承自ByteToMessageDecoder,并final实现了decode方法(变成模板方法),这里定义了自己的方法骨架,每次都调用自己新定义的decode方法(也就是我们可覆盖的方法),若有解码结果,便添加到解码输出列表中,供上层传递到下一个handler。

@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方法中,大多数代码都是用来处理 拆包、粘包、帧异常逻辑的,这一部分逻辑是共有部分,应写在方法骨架中,子类不需要关心它。

protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    //----------------判断 拆包、粘包、帧过大逻辑-从这里开始-------------------
    if (discardingTooLongFrame) {
        discardingTooLongFrame(in);
    }

    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }

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

    if (frameLength < 0) {
        failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
    }

    frameLength += lengthAdjustment + lengthFieldEndOffset;

    if (frameLength < lengthFieldEndOffset) {
        failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
    }

    if (frameLength > maxFrameLength) {
        exceededFrameLength(in, frameLength);
        return null;
    }

    // never overflows because it's less than maxFrameLength
    int frameLengthInt = (int) frameLength;
    if (in.readableBytes() < frameLengthInt) {
        return null;
    }

    if (initialBytesToStrip > frameLengthInt) {
        failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, 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);
    return frame;
}

缺点在哪儿呢:这个decode方法,不是一个private或者final的方法,它可以被子类覆盖,并且,他调用了另外一个可被覆盖的方法(extractFrame()方法,是LengthFieldBasedFrameDecoder专门留给子类覆盖的方法)

extractFrame方法的简单解释:decode方法已经分辨出哪一部分数据是一个有效帧时,调用extractFrame方法实现分帧。

 官方注释:如果确定帧内容在decode方法返回之后,再也不需要被使用,那么可以直接返回他的切片(slice),避免内存复制。

protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length)

    这里就有点坑了,识别出有效帧之后,给了一个拆分帧的extractFrame(),却没有给一个供子类覆盖的解析帧的方法

那么如何在继承的decoder里面实现解码消息呢,目前见过两种:

    1.覆盖LengthFieldBasedFrameDecoder的decode()方法,调用超类的decode()方法,若返回null表示无可用帧,返回null。若返回不为null,则表示成功拆分出一帧,然后进行帧数据解码。

    缺点:你必须得明白超类干了什么才能安全的覆盖decode方法,且不是一个好的代码风格。

@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    ByteBuf  byteBuf= (ByteBuf) super.decode(ctx, in);
    if (null==byteBuf){
        return null;
    }
    //byteBuf就是拆分出来的一帧,这里开始解析帧内容
       
    
    //
}

2.另一种带有一定的取巧性,他们也研究了源码,知道extractFrame()方法会在一帧数据可用时回调到子类,于是覆盖extractFrame()方法,直接在extractFrame()方法中解析拆分出来的帧内容,然后将解码结果发布给应用程序消费,最后返回null/或者emptyBuffer。

    缺点:代码极其混乱,他人使用你的代码的时候,需要花费很多的精力才能明白你做了什么。功能可能没有问题,但是代码质量差。

@Override
protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
    ByteBuf frame=buffer.slice(index,length);
    //frame进行解码,并发布出去,发布给消费者应用程序(不是传递到下一个handler)
    
    //
    return null;
}

那么组合使用好在哪里呢?

1.你只需要知道LengthFieldBasedFrameDecoder可以帮你处理拆包、粘包问题,并将一个个有效帧传递给下一个inboundHandler就可以了,任何具体的实现都可以不关心

2.获得更加清晰的、优雅的代码,你的代码更容易被人理解和使用。代码质量获得提升!

缺点自然也是有的:组合的方式增加了事件流,但这并不是一个重要的问题,借用一句话:你应该致力于写出更好的代码,而不是更快的代码!在你没有确定它确实给你的性能带来太多的压力时,你不应该为了更快而放弃更好的代码!

最后再强调一下:

你应该谨慎的考虑是否继承LengthFieldBasedFrameDecoder,大多数情况下你都应该以组合的方式使用它,而不是继承它!


题外话:

//------------那么LengthFieldBasedFrameDecoder的decode方法如何改进一下使得他适合继承呢?--------------------

个人看法如下:

由于它的API已经公开,直接将LengthFieldBasedFrameDecoder定义的decode方法变为final、private是不可行的,修改extractFrame的定义和方法名也是不合适。

第一步,我们为拆分出来的帧配套一个解码方法,暂称之为decode0(),为了兼容旧版本,默认实现为直接返回帧内容。

/**
 * 这里对 extractFrame() 拆分出来的帧进行解码
 * @param ctx
 * @param frame {@link #extractFrame(ChannelHandlerContext, ByteBuf,int,int)}()拆分出来的帧内容
 * @return 返回解码结果
 * @throws Exception
 */
protected Object decode0(ChannelHandlerContext ctx, ByteBuf frame) throws Exception {
    return frame;
}

第二步,修改extractFrame方法的注释,就像这样(修改之前链接的decode方法为连接到decode0()方法):

/**
 * 如果你确定该帧在{@link #decode0(ChannelHandlerContext, ByteBuf)}之后,帧内容再也不需要访问,
 * 那么你甚至可以返回他的切片视图来避免内存复制。
 * @param ctx
 * @param buffer
 * @param index
 * @param length
 * @return
 */
protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
    return buffer.retainedSlice(index,length);
}

第三步,修改decode()方法,decode方法不直接返回frame,而是返回decode0()方法的返回结果;

protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    if (discardingTooLongFrame) {
        discardingTooLongFrame(in);
    }

    if (in.readableBytes() < lengthFieldEndOffset) {
        return null;
    }

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

    if (frameLength < 0) {
        failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
    }

    frameLength += lengthAdjustment + lengthFieldEndOffset;

    if (frameLength < lengthFieldEndOffset) {
        failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
    }

    if (frameLength > maxFrameLength) {
        exceededFrameLength(in, frameLength);
        return null;
    }

    // never overflows because it's less than maxFrameLength
    int frameLengthInt = (int) frameLength;
    if (in.readableBytes() < frameLengthInt) {
        return null;
    }

    if (initialBytesToStrip > frameLengthInt) {
        failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
    }
    in.skipBytes(initialBytesToStrip);

    // extract frame
    int readerIndex = in.readerIndex();
    int actualFrameLength = frameLengthInt - initialBytesToStrip;
    //钩子方法1
    ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
    in.readerIndex(readerIndex + actualFrameLength);
    //钩子方法2
    return decode0(ctx,frame);
}

在这样修改之后,LengthFieldBasedFrameDecoder定义的decode()方法就变为了模板方法,再也不要覆盖它,我们只需要可选的覆盖流程的两个小步骤,拆分帧和解码帧。这样继承它的难度就变小了很多,而且代码也会变得简单、优雅。

以上纯个人看法,而且我也只是个萌新大笑,有错误的话欢迎指正!

猜你喜欢

转载自blog.csdn.net/weixin_37555076/article/details/80334695