Netty源码分析——编/解码

752 阅读4分钟
原文链接: wyj.shiwuliang.com

Netty源码分析——编/解码

前言

这一篇看一下Netty的编码和解码的工作原理。编码解码器其实都是一种特殊的Handler,既然是Handler,那就有inBoundoutBound的区别。

我们这篇主要是梳理一下,从入站解码到业务处理,再到出站编码的过程,让大家对编解码的流程有个了解。

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一样,但是不会校验下标的合法性,主要是为了提升性能。

注意我们在channelReadfinally块中,不管怎样都会触发一次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();
    }
}

这里比较简单了。没有很多细节,看上面基本能够看懂,就不细说了。