2021SC@SDUSC
前言
在之前的几篇博客中,分析了几种netty内置的解码器,它们都继承了ByteToMessageDecoder类,也分析了它们对消息的解码过程,也就是decode方法。在本篇博客中,只会简单地介绍ByteToMessageDecoder类调用decode的过程。
一、ByteToMessageDecoder
ByteToMessageDecoder类继承自ChannelInboundHandlerAdapter类,标识它确实是一个处理器,可以放入pipeline中。
protected ByteToMessageDecoder() {
ensureNotSharable();
}
只有一个构造方法,权限是protected。在构造方法中只做了一件事,就是检查是否有@Sharable
注解,如果有的话,抛出异常。在之前的博客中提到过,decoder,特别是继承自ByteToMessageDecoder的解码器,不能有@Sharable
注解。
之后,会分析ByteToMessageDecoder类的channelRead(ChannelHandlerContext ctx, Object msg)
方法和callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
方法。
二、callDecode
首先分析callDecode方法。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
final int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
// Check if this handler was removed before continuing with decoding.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See:
// - https://github.com/netty/netty/issues/4635
if (ctx.isRemoved()) {
break;
}
}
int oldInputLength = in.readableBytes();
decodeRemovalReentryProtection(ctx, in, out);
// Check if this handler was removed before continuing the loop.
// If it was removed, it is not safe to continue to operate on the buffer.
//
// See https://github.com/netty/netty/issues/1664
if (ctx.isRemoved()) {
break;
}
if (out.isEmpty()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
在方法中,有一个while循环,条件是in.isReadable()
,即bytebuf中还有可读数据,可以按照用户自定义的方式解析。
重点是之后进入decodeRemovalReentryProtection方法。
final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
decodeState = STATE_CALLING_CHILD_DECODE;
try {
decode(ctx, in, out);
} finally {
boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
decodeState = STATE_INIT;
if (removePending) {
fireChannelRead(ctx, out, out.size());
out.clear();
handlerRemoved(ctx);
}
}
}
该方法是final方法,子类不可修改,毕竟是模板方法模式。而在这个方法中,实际调用了decode方法。
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
这个decode方法没有任何实现,需要子类实现,为了方便回忆,这里看一下FixedLengthFrameDecoder类的重写的decode方法。
@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方法(没有继承)获取解析后的数据,再加入了out中。
而在decodeRemovalReentryProtection中,虽然不仅仅是调用了decode方法, 获取了解析后的数据,但是,剩下的部分我没有理解。
回到callDecode方法,当调用了decodeRemovalReentryProtection后,首先判断out是不是空,如果是空,那么意味着没有解析后的数据加入out。这时,如果可读字节在decode前后大小一致,说明不需要再解析了,直接退出循环;否则,如果大小不一致,有可能是读取的数据长度超过了协议的上限,数据被抛弃了,但依然需要进入下一侧循环,因为后面可能还有数据。
在这个方法中,我有一个问题没有理解,就是调用fireChannelRead
方法的时机,代码中,是在实际decode
之前先判断out中是否有数据,如果有的话,调用fireChannelRead
,将数据传递给下一个处理器。这个处理逻辑出现在while循环较前面的位置。我没有理解的是,为什么不在实际读取数据之后,再进行这个处理过程。实际上,假设在最后一次循环中读取了数据,并将解析后的数据加入了out中,之后判断in.isReadable()
,如果返回了false
,那么就不会进入循环体,也不会去触发fireChannelRead
方法,好像会在之后其他的方法中去调用,但为什么不选择更改代码的顺序,这里我不能理解。
三、channelRead
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
first = cumulation == null;
cumulation = cumulator.cumulate(ctx.alloc(),
first ? Unpooled.EMPTY_BUFFER : cumulation, (ByteBuf) msg);
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
try {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
} finally {
out.recycle();
}
}
} else {
ctx.fireChannelRead(msg);
}
}
首先判断msg是不是ByteBuf类的实例,如果不是的话,直接交给下一个处理器,所以一般建议在initChannel
的时候,将解码器写在其它处理器的前面。
吐过msg是ByteBuf类的实例,实现创建一个CodecOutputList 类的对象out,之后,判断当前cummulation是否为空,如果是的话,说明是第一次,first为true。之后,将之前的数据与这次的数据连接起来,这里涉及是不是第一批数据的问题,逻辑比较简单。然后调用callDecode方法,在上面分析过了。
之后,进入finally语句块。根具条件,将资源释放,之后,将数据传递给下一个处理器。
四、总结
在本篇博客中,简单介绍了ByteToMessageDecoder类,并且,主要分析了调用decode方法的过程,可以清楚地看出,这是模板方法的设计模式,即父类定义了处理逻辑,而将某些过程的具体实现留给子类实现。显然,在本篇博客中,关于ByteToMessageDecoder类,有很多地方没有去分析。