先上结论:你应当优先使用组合的方式使用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()方法就变为了模板方法,再也不要覆盖它,我们只需要可选的覆盖流程的两个小步骤,拆分帧和解码帧。这样继承它的难度就变小了很多,而且代码也会变得简单、优雅。
以上纯个人看法,而且我也只是个萌新,有错误的话欢迎指正!