Netty源码分析——编/解码
前言
这一篇看一下Netty的编码和解码的工作原理。编码解码器其实都是一种特殊的Handler,既然是Handler,那就有inBound和outBound的区别。
我们这篇主要是梳理一下,从入站解码到业务处理,再到出站编码的过程,让大家对编解码的流程有个了解。
Decoder
我们随便挑选一种Decoder看一下实现就可以了,比较简单。这篇的内容相比之前的都简单一些、轻松一些。
选ByteToMessageDecoder看一下,首先这个Decoder继承自ChannelInboundHandlerAdapter,这个类其实是一个InBoundHandler,实际上处理的是读事件。
那么看看这个Decoder是怎么把数据转成我们想要的类对象的。先看下channelRead方法:
// 只能处理ByteBuf
if (msg instanceof ByteBuf) {
// 这是一个继承自ArrayList的数据结构
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
//第一次解码,cumulation是之前没解码完成的数据
cumulation = data;
} else {
//把这次的数据加到cumulation里等待被解码
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
//真正解码的部分
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// channelRead执行了如果大于16次,但是数据仍然还存在在cumulation里,这里跳过这些字节,避免OOM
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
//最后还是会执行一次channelRead把out里的数据向后传递
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
这里的步骤拆解一下。首先,ByteToMessageDecoder只能处理ByteBuf。然后创建一个CodecOutputList这个数据结构。这里这个结构其实就是一个ArrayList,但是其中的所有方法都重写了。区别不是很大,但是这个类有一个getUnsafe方法,传入的index不会被校验。这里这个数据结构主要是为了提升性能。
然后把数据放到cumulation里。这其实是一个ByteBuf,保存的是所有还没被解码的数据。
然后进行真正的解码操作。这里需要注意finally里的一段代码,是避免OOM的,这说的是一种场景。如果这个cumulation一直有数据可以读(一直没被解码),channelRead调用了超过16次,这个cumulation中的数据还没被读完,这里基本认为是发生了内存泄露。我举个例子,比如我有个Decoder继承了个这个ByteToMessageDecoder,但是decode方法我不从ByteBuf里读数据,而是直接向out里写一个数据,那这个cumulation就会越来越大。
Netty为了避免这种情况产生OOM,会扔掉这部分数据(执行discardSomeReadBytes)。
我们看下callDecode方法:
while (in.isReadable()) {
//第一次out是new出来的 没有数据
int outSize = out.size();
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
//调用decode方法,被解码的数据放到out里
decodeRemovalReentryProtection(ctx, in, out);
// 如果这个节点被移除了,就不继续操作数据了
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(“...");
}
if (isSingleDecode()) {
break;
}
}
这里简单看一下,如果out里本来就有数据,就触发channelRead,把数据向后传递:
static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
if (msgs instanceof CodecOutputList) {
// 这里的方法就是下面的fireChannelRead
fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
} else {
for (int i = 0; i < numElements; i++) {
ctx.fireChannelRead(msgs.get(i));
}
}
}
//把out中的数据都向后传递
static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
}
注意这里是把out里的每个数据都向后传递。注意这里第二个fireChannelRead用的是CodecOutputList.getUnsafe,这个Unsafe方法的入参就是数组下标,跟ArrayList一样,但是不会校验下标的合法性,主要是为了提升性能。
注意我们在channelRead的finally块中,不管怎样都会触发一次fireChannelRead。
Encoder
同样选MessageToByteEncoder,实际上是一个outBoundHandler。看write方法:
ByteBuf buf = null;
try {
if (acceptOutboundMessage(msg)) {
// 出站msg是否能够被encoder处理。。
I cast = (I) msg;
buf = allocateBuffer(ctx, cast, preferDirect);
try {
// 编码
encode(ctx, cast, buf);
} finally {
//编码之后释放之前的msg
ReferenceCountUtil.release(cast);
}
if (buf.isReadable()) {
// 向后传递
ctx.write(buf, promise);
} else {
// 如果不可读,则向后传递一个buffer
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
// 不能处理直接向后传递
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
// 释放一下
if (buf != null) {
buf.release();
}
}
这里比较简单了。没有很多细节,看上面基本能够看懂,就不细说了。