netty源码之ChannelHandler

ChannelHandler类似于Servlet的Filter过滤器,负责对I/O事件或者IO操作进行拦截和处理,它可以选择性地拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。

基于ChannelHandler接口,用户可以方便地进行业务逻辑定制,例如打印日志、统一封装异常信息、性能统计和消息编解码等。

ChannelHandler

ChannelHandler的继承关系:

netty源码之ChannelHandler_子类

由于ChannelHandler是Netty框架和用户代码的主要扩展和定制点,所以它的子类种类繁多、功能各异,系统ChannelHandler主要分类如下:

  • ChannelPipeline中的系统ChannelHandler,用于I/O操作和对事件进行预处理,对于用户不可见,这类ChannelHandler主要包括HeadHandle和TailHandler。
  • 编解码ChannelHandler,包括ByteToMessageCodec、MessageToMessageDecoder等,这些编解码类本身又包含多种子类。
  • 其他系统功能性ChannelHandler,包括读写超时Handler、日志Handler等。
  • 协议编解码Handler,Netty事先实现了很多协议的ChannelHandler,比如Http协议相关、SSL相关等等。

ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter

对于大多数的ChannelHandler会选择性地拦截和处理某个或者某些事件,其他的事件会忽略,由下一个ChannelHandler进行拦截和处理。这就会导致一个问题:用户ChannelHandler必须要实现ChannelHandler的所有接口,包括它不关心的那些事件处理接口,这会导致用户代码的冗余和臃肿,代码的可维护性也会变差。

为了解决这个问题,Netty提供了ChannelHandlerAdapter基类,它的实现要么就是事件透传,要么就是空实现,如果用户ChannelHandler关心某个事件,只需要覆盖ChannelHandlerAdapter对应的方法即可,对于不关心的,可以直接继承使用父类的方法,这样子类的代码就会非常简洁和清晰。

我们的ChannelHandler都是直接继承自ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter,开发起来非常简单和高效。

channelRead与channelReadComplete的区别

下面用代码来验证一下:

Server端添加如下Handler:

ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new LineBasedFrameDecoder(128));
ch.pipeline().addLast(new ServerHandler());

ServerHandler中的代码如下:

@Slf4j
public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        log.info("channelRead: {}", byteBuf.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        log.info("channelReadComplete");
    }
}

ClientHandler端的代码如下:

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        for (int i = 0; i < 12; i++) {
            ByteBuf sendByteBuf = Unpooled.copiedBuffer(("helloworldnetty\n").getBytes());
            ctx.writeAndFlush(sendByteBuf);
        }
    }
}

Server收到的数据如下:

2020-12-01 17:05:21,119 DEBUG [nioEventLoopGroup-3-1] (AbstractInternalLogger.java:145) - [id: 0x7b4bdd32, L:/127.0.0.1:8899 - R:/127.0.0.1:12903] READ: 192B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000010| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000020| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000030| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000040| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000050| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000060| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000070| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000080| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|00000090| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|000000a0| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
|000000b0| 68 65 6c 6c 6f 77 6f 72 6c 64 6e 65 74 74 79 0a |helloworldnetty.|
+--------+-------------------------------------------------+----------------+
2020-12-01 17:05:21,128  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,129  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:15) - channelRead: helloworldnetty
2020-12-01 17:05:21,130 DEBUG [nioEventLoopGroup-3-1] (AbstractInternalLogger.java:145) - [id: 0x7b4bdd32, L:/127.0.0.1:8899 - R:/127.0.0.1:12903] READ COMPLETE
2020-12-01 17:05:21,130  INFO [nioEventLoopGroup-3-1] (ServerHandler.java:20) - channelReadComplete

从运行结果可以发现channelRead执行了12次,而channelReadComplete只执行了一次。

总结两者的区别:

  • channelReadComplete:每次读取完Socket的接收缓冲区的报文,channelReadComplete会被触发一次,注意这里是Socket的接收缓冲区,不是Netty中接收的ByteBuf的大小。
  • channelRead:从Netty的设计角度和实现来看,当收到一个完整的应用层消息报文,channelRead会被触发一次,也就是读取到一条你需要的完整数据就会触发一次。

如果每次传递的完整的数据很小,那么ByteBuf中就会存储多个完整的数据,那么channelRead就会被回调多次,如果传递的完整的数据很大,那么有可能存在调用三次channelReadComplete才会调用一次channelRead的情况。

ByteToMessageDecoder、MessageToByteEncoder

ByteToMessageDecoder和MessageToByteEncoder是我们用的比较多的两个ChannelHandler的子类,我们挑这两个子类来看看里面关键的方法。

ByteToMessageDecoder

ByteToMessageDecoder用于将ByteBuf转换成Message,message可以是POJO等等,转换后继续在ChannelPipeline中往后面的入站Handler传递,ByteToMessageDecoder继承自ChannelInboundHandlerAdapter,需要开发者实现的是decode方法:

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

in是用来读取的数据,out是我们转换后的对象列表。

ByteToMessage是ChannelInboundHandler,很明显,关键的方法是channelRead:

// io.netty.handler.codec.ByteToMessageDecoder#channelRead
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);
    }
}

从总体结构来说,

if (msg instanceof ByteBuf) {
// (略)这里是第二层结构
} else {
	ctx.fireChannelRead(msg);
}

很明显,判断传入类型是否为ByteBuf,类型符合则处理,不符合则透传。

在具体的处理部分中,创建一个 CodecOutputList(也是一种 list)保存转码后的对象列表。因为有粘包和半包需要处理,使用ByteBuf型的cumulation来保留处理不完的ByteBuf,同时使用一个专门的接口Cumulator来负责合并。

之后调用callDecode(ctx, cumulation, out),callDecode中循环调用我们要实现的抽象方法decode(ctx,in,out)来解码。然后将解码后的对象通过fireChannelRead交给后面的Handler进行处理。

MessageToByteEncoder

MessageToByteEncoder用于将Message转换成ByteBuf,转换后继续在ChannelPipeline中往前面的出站Handler传递,MessageToByteEncoder继承自ChannelOutboundHandlerAdapter,需要开发者实现的是encode方法。

protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;

MessageToByteEncoder是一个ChannelOutboundHandler,里面关键的就是write方法:

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                encode(ctx, cast, buf);
            } finally {
                ReferenceCountUtil.release(cast);
            }

            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                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();
        }
    }
}

其中的逻辑是比较简单的,判断消息是否是类型匹配的,不是就直接往前面的出站handler传递,是的话会申请一个缓冲区,然后调用开发者实现encode方法进行编码,编码成功,通过ctx往前面的出站Handler进行传递,并会释放消息。如果编码失败,就往前写一个空缓冲区,把申请的缓冲区释放了。