从零开始实现简易版Netty(二) MyNetty pipeline流水线

从零开始实现简易版Netty(二) MyNetty pipeline流水线

1. Netty pipeline流水线介绍

在上一篇博客中lab1版本的MyNetty参考netty实现了一个极其精简的reactor模型。按照计划,lab2版本的MyNetty需要实现pipeline流水线,以支持不同的逻辑处理模块的解耦。

由于本文属于系列博客,读者需要对之前的博客内容有所了解才能更好地理解本文内容。

在lab1版本中,MyNetty的EventLoop处理逻辑中,允许使用者配置一个EventHandler,并在处理read事件时调用其实现的自定义fireChannelRead方法。

这一机制在实现demo中的简易echo服务器时是够用的,但在实际的场景中,一个完备的网络程序,业务想要处理的IO事件有很多类型,并且不希望在一个大而全的臃肿的处理器中处理所有的IO事件,而是能够模块化的拆分不同的处理逻辑,实现架构上的灵活解耦。
因此netty提供了pipeline流水线机制,允许用户在使用netty时能按照自己的需求,按顺序组装自己的处理器链条。

1.1 netty的IO事件

在实际的网络环境中,有非常多不同类型的IO事件,最典型的就是读取来自远端的数据(read)以及向远端写出发送数据(write)。

netty对这些IO事件进行了抽象,并允许用户编写自定义的处理器监听或是主动触发这些事件。

netty按照事件数据流的传播方向将IO事件分成了入站(InBound)与出站(OutBound)两大类,由远端输入传播到本地应用程序的事件被叫做入站事件,从本地应用程序触发向远端传播的事件叫出站事件。

主要的入站事件有channelRead、channelActive等,主要的出站事件有write、connect、bind等。

1.2 netty的IO事件处理器与pipeline流水线

针对InBound入站IO事件,netty抽象出了ChannelInboundHandler接口;针对OutBound出站IO事件,netty抽象出了ChannelOutboundHandler接口。

用户可以编写一系列继承自对应ChannelHandler接口的自定义处理器,将其绑定到ChannelPipeline中。每一个Channel都对应一个ChannelPipeline,ChannelPipeline实例是独属于某个特定channel连接的。

2. MyNetty实现pipeline流水线

经过上述对于netty的IO事件与pipeline流水线简要介绍后,读者对netty的流水线虽然有了一定的概念,但对具体的细节还是知之甚少。下面我们结合MyNetty的源码,展开介绍netty的流水线机制实现。

2.1 MyNetty的事件处理器

java 复制代码
/**
 * 事件处理器(相当于netty中ChannelInboundHandler和ChannelOutboundHandler合在一起)
 * */
public interface MyChannelEventHandler {

    // ========================= inbound入站事件 ==============================
    void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception;

    void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) throws Exception;

    // ========================= outbound出站事件 ==============================
    void close(MyChannelHandlerContext ctx) throws Exception;

    void write(MyChannelHandlerContext ctx, Object msg) throws Exception;
}
  • 前面说到,netty将入站与出站事件用两个不同的ChannelEventHandler接口进行了抽象,而在MyNetty中因为最终要支持的IO事件没有netty那么多,所以出站、入站的处理接口进行了合并。
    这样做虽然在架构上不如netty那样拆分开来的设计优雅,但相对来说理解起来会更加简单。
  • 未来MyChannelEventHandler还会随着迭代支持更多的IO事件,但这是个渐进的过程,目前lab2中只需要支持少数几个IO事件便能满足需求。

2.2 MyNetty的pipeline流水线与ChannelHandler上下文

MyNetty pipeline流水线实现
java 复制代码
public interface MyChannelEventInvoker {

    // ========================= inbound入站事件 ==============================
    void fireChannelRead(Object msg);

    void fireExceptionCaught(Throwable cause);

    // ========================= outbound出站事件 ==============================
    void close();

    void write(Object msg);
}
java 复制代码
/**
 * pipeline首先自己也是一个Invoker
 *
 * 包括head和tail两个哨兵节点
 * */
public class MyChannelPipeline implements MyChannelEventInvoker {

    private static final Logger logger = LoggerFactory.getLogger(MyChannelPipeline.class);

    private final MyNioChannel channel;

    /**
     * 整条pipeline上的,head和tail两个哨兵节点
     *
     * inbound入站事件默认都从head节点开始向tail传播
     * outbound出站事件默认都从tail节点开始向head传播
     * */
    private final MyAbstractChannelHandlerContext head;
    private final MyAbstractChannelHandlerContext tail;

    public MyChannelPipeline(MyNioChannel channel) {
        this.channel = channel;

        head = new MyChannelPipelineHeadContext(this);
        tail = new MyChannelPipelineTailContext(this);

        head.setNext(tail);
        tail.setPrev(head);
    }

    @Override
    public void fireChannelRead(Object msg) {
        // 从head节点开始传播读事件(入站)
        MyChannelPipelineHeadContext.invokeChannelRead(head,msg);
    }

    @Override
    public void fireExceptionCaught(Throwable cause) {
        // 异常传播到了pipeline的末尾,打印异常信息
        onUnhandledInboundException(cause);
    }

    @Override
    public void close() {
        // 出站事件,从尾节点向头结点传播
        tail.close();
    }

    @Override
    public void write(Object msg) {
        tail.write(msg);
    }

    public void addFirst(MyChannelEventHandler handler){
        // 非sharable的handler是否重复加入的校验
        checkMultiplicity(handler);

        MyAbstractChannelHandlerContext newCtx = newContext(handler);

        MyAbstractChannelHandlerContext oldFirstCtx = head.getNext();
        newCtx.setPrev(head);
        newCtx.setNext(oldFirstCtx);
        head.setNext(newCtx);
        oldFirstCtx.setPrev(newCtx);
    }

    public void addLast(MyChannelEventHandler handler){
        // 非sharable的handler是否重复加入的校验
        checkMultiplicity(handler);

        MyAbstractChannelHandlerContext newCtx = newContext(handler);

        // 加入链表尾部节点之前
        MyAbstractChannelHandlerContext oldLastCtx = tail.getPrev();
        newCtx.setPrev(oldLastCtx);
        newCtx.setNext(tail);
        oldLastCtx.setNext(newCtx);
        tail.setPrev(newCtx);
    }

    private static void checkMultiplicity(MyChannelEventHandler handler) {
        if (handler instanceof MyChannelEventHandlerAdapter) {
            MyChannelEventHandlerAdapter h = (MyChannelEventHandlerAdapter) handler;

            if (!h.isSharable() && h.added) {
                // 一个handler实例不是sharable,但是被加入到了pipeline一次以上,有问题
                throw new MyNettyException(
                        h.getClass().getName() + " is not a @Sharable handler, so can't be added or removed multiple times.");
            }

            // 第一次被引入,当前handler实例标记为已加入
            h.added = true;
        }
    }

    public MyNioChannel getChannel() {
        return channel;
    }

    private void onUnhandledInboundException(Throwable cause) {
        logger.warn("An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                "It usually means the last handler in the pipeline did not handle the exception.", cause);
    }

    private MyAbstractChannelHandlerContext newContext(MyChannelEventHandler handler) {
        return new MyDefaultChannelHandlerContext(this,handler);
    }
}
  • pipeline实现了ChannelEventInvoker接口,ChannelEventInvoker与ChannelEventHandler中对应IO事件的方法是一一对应的,唯一的区别在于其方法中缺失了(MyChannelHandlerContext ctx)参数。
    Invoker接口用于netty内部触发流水线的事件传播,而Handler接口用于用户自定义IO事件触发时的事件处理器。
  • 同时,pipeline流水线中定义了两个关键属性,head和tail,其都是AbstractChannelHandlerContext类型的,其内部工作原理我们在下一小节展开。
    pipeline提供了addFirst和addLast两个方法(netty中提供了非常多功能类似的方法,MyNetty简单起见只实现了最常用的两个),允许将用户自定义的ChannelHandler挂载在pipeline中,与head、tail组成一个双向链表,而入站出站事件会按照双向链表中节点的顺序进行传播。
  • 对于入站事件(比如fireChannelRead),事件从head节点开始,从前到后的在流水线的handler链表中传播;而出站事件(比如write), 事件则从tail节点开始,从后往前的在流水线的handler链表中传播。

2.3 MyNetty ChannelHandlerContext上下文实现

下面我们来深入讲解ChannelHandlerContext上下文原理,看看一个具体的事件在pipeline的双向链表中的传播是如何实现的。

MyChannelHandlerContext上下文接口定义
java 复制代码
public interface MyChannelHandlerContext extends MyChannelEventInvoker {

    MyNioChannel channel();

    MyChannelEventHandler handler();

    MyChannelPipeline getPipeline();

    MyNioEventLoop executor();
}
MyAbstractChannelHandlerContext上下文骨架类
java 复制代码
public abstract class MyAbstractChannelHandlerContext implements MyChannelHandlerContext{

    private static final Logger logger = LoggerFactory.getLogger(MyAbstractChannelHandlerContext.class);

    private final MyChannelPipeline pipeline;

    private final int executionMask;

    /**
     * 双向链表前驱/后继节点
     * */
    private MyAbstractChannelHandlerContext prev;
    private MyAbstractChannelHandlerContext next;

    public MyAbstractChannelHandlerContext(MyChannelPipeline pipeline, Class<? extends MyChannelEventHandler> handlerClass) {
        this.pipeline = pipeline;

        this.executionMask = MyChannelHandlerMaskManager.mask(handlerClass);
    }

    @Override
    public MyNioChannel channel() {
        return pipeline.getChannel();
    }

    public MyAbstractChannelHandlerContext getPrev() {
        return prev;
    }

    public void setPrev(MyAbstractChannelHandlerContext prev) {
        this.prev = prev;
    }

    public MyAbstractChannelHandlerContext getNext() {
        return next;
    }

    public void setNext(MyAbstractChannelHandlerContext next) {
        this.next = next;
    }

    @Override
    public MyNioEventLoop executor() {
        return this.pipeline.getChannel().getMyNioEventLoop();
    }

    @Override
    public void fireChannelRead(Object msg) {
        // 找到当前链条下最近的一个支持channelRead方法的MyAbstractChannelHandlerContext(inbound事件,从前往后找)
        MyAbstractChannelHandlerContext nextHandlerContext = findContextInbound(MyChannelHandlerMaskManager.MASK_CHANNEL_READ);

        // 调用找到的那个ChannelHandlerContext其handler的channelRead方法
        MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
        if(myNioEventLoop.inEventLoop()){
            invokeChannelRead(nextHandlerContext,msg);
        }else{
            // 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
            myNioEventLoop.execute(()->{
                invokeChannelRead(nextHandlerContext,msg);
            });
        }
    }

    @Override
    public void fireExceptionCaught(Throwable cause) {
        // 找到当前链条下最近的一个支持exceptionCaught方法的MyAbstractChannelHandlerContext(inbound事件,从前往后找)
        MyAbstractChannelHandlerContext nextHandlerContext = findContextInbound(MyChannelHandlerMaskManager.MASK_EXCEPTION_CAUGHT);

        // 调用找到的那个ChannelHandlerContext其handler的exceptionCaught方法

        MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
        if(myNioEventLoop.inEventLoop()){
            invokeExceptionCaught(nextHandlerContext,cause);
        }else{
            // 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
            myNioEventLoop.execute(()->{
                invokeExceptionCaught(nextHandlerContext,cause);
            });
        }
    }

    @Override
    public void close() {
        // 找到当前链条下最近的一个支持close方法的MyAbstractChannelHandlerContext(outbound事件,从后往前找)
        MyAbstractChannelHandlerContext nextHandlerContext = findContextOutbound(MyChannelHandlerMaskManager.MASK_CLOSE);

        MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
        if(myNioEventLoop.inEventLoop()){
            doClose(nextHandlerContext);
        }else{
            // 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
            myNioEventLoop.execute(()->{
                doClose(nextHandlerContext);
            });
        }
    }

    private void doClose(MyAbstractChannelHandlerContext nextHandlerContext){
        try {
            nextHandlerContext.handler().close(nextHandlerContext);
        } catch (Throwable t) {
            logger.error("{} do close error!",nextHandlerContext,t);
        }
    }

    @Override
    public void write(Object msg) {
        // 找到当前链条下最近的一个支持write方法的MyAbstractChannelHandlerContext(outbound事件,从后往前找)
        MyAbstractChannelHandlerContext nextHandlerContext = findContextOutbound(MyChannelHandlerMaskManager.MASK_WRITE);

        MyNioEventLoop myNioEventLoop = nextHandlerContext.executor();
        if(myNioEventLoop.inEventLoop()){
            doWrite(nextHandlerContext,msg);
        }else{
            // 防并发,每个针对channel的操作都由自己的eventLoop线程去执行
            myNioEventLoop.execute(()->{
                doWrite(nextHandlerContext,msg);
            });
        }
    }

    private void doWrite(MyAbstractChannelHandlerContext nextHandlerContext, Object msg) {
        try {
            nextHandlerContext.handler().write(nextHandlerContext,msg);
        } catch (Throwable t) {
            logger.error("{} do write error!",nextHandlerContext,t);
        }
    }

    @Override
    public MyChannelPipeline getPipeline() {
        return pipeline;
    }

    public static void invokeChannelRead(MyAbstractChannelHandlerContext next, Object msg) {
        try {
            next.handler().channelRead(next, msg);
        }catch (Throwable t){
            // 处理抛出的异常
            next.invokeExceptionCaught(t);
        }
    }

    public static void invokeExceptionCaught(MyAbstractChannelHandlerContext next, Throwable cause) {
        next.invokeExceptionCaught(cause);
    }

    private void invokeExceptionCaught(final Throwable cause) {
        try {
            this.handler().exceptionCaught(this, cause);
        } catch (Throwable error) {
            // 如果捕获异常的handler依然抛出了异常,则打印debug日志
            logger.error(
                "An exception {}" +
                    "was thrown by a user handler's exceptionCaught() " +
                    "method while handling the following exception:",
                ThrowableUtil.stackTraceToString(error), cause);
        }
    }

    private MyAbstractChannelHandlerContext findContextInbound(int mask) {
        MyAbstractChannelHandlerContext ctx = this;
        do {
            // inbound事件,从前往后找
            ctx = ctx.next;
        } while (needSkipContext(ctx, mask));

        return ctx;
    }

    private MyAbstractChannelHandlerContext findContextOutbound(int mask) {
        MyAbstractChannelHandlerContext ctx = this;
        do {
            // outbound事件,从后往前找
            ctx = ctx.prev;
        } while (needSkipContext(ctx, mask));

        return ctx;
    }

    private static boolean needSkipContext(MyAbstractChannelHandlerContext ctx, int mask) {
        // 如果与运算后为0,说明不支持对应掩码的操作,需要跳过
        return (ctx.executionMask & (mask)) == 0;
    }
}
MyChannelPipelineHeadContext pipeline哨兵头结点
java 复制代码
/**
 * pipeline的head哨兵节点
 * */
public class MyChannelPipelineHeadContext extends MyAbstractChannelHandlerContext implements MyChannelEventHandler {

    public MyChannelPipelineHeadContext(MyChannelPipeline pipeline) {
        super(pipeline,MyChannelPipelineHeadContext.class);
    }

    @Override
    public void channelRead(MyChannelHandlerContext ctx, Object msg) {
        ctx.fireChannelRead(msg);
    }

    @Override
    public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) {
        ctx.fireExceptionCaught(cause);
    }

    @Override
    public void close(MyChannelHandlerContext ctx) throws Exception {
        // 调用jdk原生的channel方法,关闭掉连接
        ctx.getPipeline().getChannel().getJavaChannel().close();
    }

    @Override
    public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
        // 往外写的操作,一定是socketChannel
        SocketChannel socketChannel = (SocketChannel) ctx.getPipeline().getChannel().getJavaChannel();

        if(msg instanceof ByteBuffer){
            socketChannel.write((ByteBuffer) msg);
        }else{
            // msg走到head节点的时候,必须是ByteBuffer类型
            throw new Error();
        }
    }

    @Override
    public MyChannelEventHandler handler() {
        return this;
    }
}
MyChannelPipelineHeadContext pipeline哨兵尾结点
java 复制代码
/**
 * pipeline的tail哨兵节点
 * */
public class MyChannelPipelineTailContext extends MyAbstractChannelHandlerContext implements MyChannelEventHandler {

    private static final Logger logger = LoggerFactory.getLogger(MyChannelPipelineTailContext.class);

    public MyChannelPipelineTailContext(MyChannelPipeline pipeline) {
        super(pipeline, MyChannelPipelineTailContext.class);
    }

    @Override
    public void channelRead(MyChannelHandlerContext ctx, Object msg) {
        // 如果channelRead事件传播到了tail节点,说明用户自定义的handler没有处理好,但问题不大,打日志警告下
        onUnhandledInboundMessage(ctx,msg);
    }

    @Override
    public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) {
        // 如果exceptionCaught事件传播到了tail节点,说明用户自定义的handler没有处理好,但问题不大,打日志警告下
        onUnhandledInboundException(cause);
    }

    @Override
    public void close(MyChannelHandlerContext ctx) throws Exception {
        // do nothing
        logger.info("close op, tail context do nothing");
    }

    @Override
    public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
        // do nothing
        logger.info("write op, tail context do nothing");
    }

    @Override
    public MyChannelEventHandler handler() {
        return this;
    }

    private void onUnhandledInboundException(Throwable cause) {
        logger.warn(
            "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                "It usually means the last handler in the pipeline did not handle the exception.",
            cause);
    }

    private void onUnhandledInboundMessage(MyChannelHandlerContext ctx, Object msg) {
        logger.debug(
            "Discarded inbound message {} that reached at the tail of the pipeline. " +
                "Please check your pipeline configuration.", msg);

        logger.debug("Discarded message pipeline : {}. Channel : {}.",
            ctx.getPipeline(), ctx.channel());
    }
}
  • AbstractChannelHandlerContext作为ChannelHandlerContext子类的基础骨架,是理解Netty中IO事件传播机制的重中之重。AbstractChannelHandlerContext做为ChannelPipeline的实际节点,其拥有prev和next两个属性,用于关联链表中的前驱和后继。
  • 在触发IO事件时,AbstractChannelHandlerContext会按照一定的规则(具体原理在下一节展开)找到下一个需要处理当前类型IO事件的事件处理器(findContextInbound与findContextOutBound方法)。
  • 在找到后会先判断当前线程与目标MyAbstractChannelHandlerContext的执行器线程是否相同(inEventLoop),如果是则直接触发对应handler的回调方法;如果不是则将当前事件包装成一个任务交给next节点的executor执行。
    这样设计的主要原因是netty作为一个高性能网络框架,是非常忌讳使用同步锁的。EventLoop线程是按照引入taskQueue队列多写单读的方式消费IO事件以及相关任务的,这样可以避免处理IO事件时防止不同线程间并发而大量加锁。
  • 举个例子,一个聊天服务器,用户a通过连接A发送了一条消息给服务端,而服务端需要通过连接b将消息同步给用户b,连接a和连接b属于不同的EventLoop线程。
    连接a所在的EventLoop在接受到读事件后,需要往连接b写出数据,此时不能直接由连接a的线程执行channel的写出操作(inEventLoop为false),而必须通过execute方法写入taskQueue交给管理连接b的EventLoop线程,让它异步的处理。
    试想如果能允许别的EventLoop线程来回调触发不属于它的channel的IO事件,那么所有的ChannelHandler都必须考虑多线程并发的问题而被迫引入同步机制,导致性能大幅降低。
  • netty中可以在ChannelHandler中主动的触发一些IO事件,比如write写出事件。如果是使用ChannelHandlerContext.write写出,则传播的起点是当前Handler节点;而如果是ChannelHandlerContext.channel.write的方式写出,其底层就是调用的是pipeline.write,其传播的起点则是tail哨兵节点。
    结合MyNetty中上述pipeline相关的代码,相信读者应该能更好的理解netty中的这一传播机制。

2.4 ChannelHandler mask掩码过滤机制

通常情况,用户自定义的IO事件处理器一般都是各司其职的,不会对每一种IO事件都感兴趣。比如最经典的编解码handler,一般来说encode编码处理器只关心写出到远端的出站事件,而decode解码处理器只关心读取到数据的入站事件。

但编码、解码处理器都是位于pipeline的同一个链表中的,因此IO事件理论上会在链表中的所有处理器中传播。同时由于netty允许ChannelHandler在内部自行决定是否将事件往下一个handler节点传播,因此如果不引入特别的机制,则意味着用户自定义的每一个ChannelHandler都必须实现所有的接口方法,并在内部添加模版代码来确保事件能够继续在pipeline中传播(比如都必须实现fireChannelRead方法,并且都调用ctx.fireChannelRead方法让事件能向后传播)。

netty中为ChannelHandler定义了非常多的IO事件接口,如果每个ChannelHandler都必须实现所有的IO事件接口,netty的用户在实现自定义处理器时会非常痛苦,同时在高并发下不必要的方法调用也会对性能有所影响。

为了解决上述问题,netty提供了Skip机制,允许用户在编写自定义处理器时仅关心自己感兴趣的IO事件,而其它事件在进行传播时能自动的跳过当前handler节点在pipeline中继续传播。

在2.3的AbstractChannelHandlerContext实现中,可以发现事件传播的过程中关键的两个方法(findContextInbound/findContextOutbound)都是基于needSkipContext方法来实现的。

needSkipContext方法中基于AbstractChannelHandlerContext中的一个属性executionMask来决定是否需要跳过某个ChannelHandler。

下面我们结合MyNetty的源码来看看这个executionMask属性是如何被计算得出,又是如何基于该掩码进行handler过滤的。

java 复制代码
/**
 * 计算并缓存每一个类型的handler所需要处理方法掩码的管理器
 *
 * 参考自netty的ChannelHandlerMask类
 * */
public class MyChannelHandlerMaskManager {

    private static final Logger logger = LoggerFactory.getLogger(MyChannelHandlerMaskManager.class);

    public static final int MASK_EXCEPTION_CAUGHT = 1;

    // ==================== inbound ==========================
    public static final int MASK_CHANNEL_REGISTERED = 1 << 1;
    public static final int MASK_CHANNEL_UNREGISTERED = 1 << 2;
    public static final int MASK_CHANNEL_ACTIVE = 1 << 3;
    public static final int MASK_CHANNEL_INACTIVE = 1 << 4;
    public static final int MASK_CHANNEL_READ = 1 << 5;
    public static final int MASK_CHANNEL_READ_COMPLETE = 1 << 6;
//    static final int MASK_USER_EVENT_TRIGGERED = 1 << 7;
//    static final int MASK_CHANNEL_WRITABILITY_CHANGED = 1 << 8;

    // ===================== outbound =========================
    public static final int MASK_BIND = 1 << 9;
    public static final int MASK_CONNECT = 1 << 10;
//    static final int MASK_DISCONNECT = 1 << 11;
    public static final int MASK_CLOSE = 1 << 12;
//    static final int MASK_DEREGISTER = 1 << 13;
    public static final int MASK_READ = 1 << 14;
    public static final int MASK_WRITE = 1 << 15;
    public static final int MASK_FLUSH = 1 << 16;

    private static final ThreadLocal<Map<Class<? extends MyChannelEventHandler>, Integer>> MASKS =
        ThreadLocal.withInitial(() -> new WeakHashMap<>(32));

    public static int mask(Class<? extends MyChannelEventHandler> clazz) {
        // 对于非共享的handler,会随着channel的创建而被大量创建
        // 为了避免反复的计算同样类型handler的mask掩码而引入缓存,优先从缓存中获得对应处理器类的掩码
        Map<Class<? extends MyChannelEventHandler>, Integer> cache = MASKS.get();
        Integer mask = cache.get(clazz);
        if (mask == null) {
            // 缓存中不存在,计算出对应类型的掩码值
            mask = calculateChannelHandlerMask(clazz);
            cache.put(clazz, mask);
        }
        return mask;
    }

    private static int calculateChannelHandlerMask(Class<? extends MyChannelEventHandler> handlerType) {
        int mask = 0;

        // MyChannelEventHandler中的方法一一对应,如果支持就通过掩码的或运算将对应的bit位设置为1

        if(!needSkip(handlerType,"channelRead", MyChannelHandlerContext.class,Object.class)){
            mask |= MASK_CHANNEL_READ;
        }

        if(!needSkip(handlerType,"exceptionCaught", MyChannelHandlerContext.class,Throwable.class)){
            mask |= MASK_EXCEPTION_CAUGHT;
        }

        if(!needSkip(handlerType,"close", MyChannelHandlerContext.class)){
            mask |= MASK_CLOSE;
        }

        if(!needSkip(handlerType,"write", MyChannelHandlerContext.class,Object.class)){
            mask |= MASK_WRITE;
        }

        return mask;
    }

    private static boolean needSkip(Class<?> handlerType, String methodName, Class<?>... paramTypes) {
        try {
            Method method = handlerType.getMethod(methodName, paramTypes);

            // 如果有skip注解,说明需要跳过
            return method.isAnnotationPresent(Skip.class);
        } catch (NoSuchMethodException e) {
            // 没有这个方法,就不需要设置掩码
            return false;
        }
    }
}
java 复制代码
/**
 * 用于简化用户自定义的handler的适配器
 *
 * 由于所有支持的方法都加上了@Skip注解,子类只需要重写想要关注的方法即可,其它未重写的方法将会在事件传播时被跳过
 * */
public class MyChannelEventHandlerAdapter implements MyChannelEventHandler{

    /**
     * 当前是否已经被加入sharable缓存
     * */
    public volatile boolean added;

    @Skip
    @Override
    public void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }

    @Skip
    @Override
    public void exceptionCaught(MyChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
    }

    @Skip
    @Override
    public void close(MyChannelHandlerContext ctx) throws Exception {
        ctx.close();
    }

    @Skip
    @Override
    public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.write(msg);
    }


    private static ConcurrentHashMap<Class<?>, Boolean> isSharableCacheMap = new ConcurrentHashMap<>();

    public boolean isSharable() {
        /**
         * MyNetty中直接用全局的ConcurrentHashMap来缓存handler类是否是sharable可共享的,实现起来很简单
         * 而netty中利用FastThreadLocal做了优化,避免了不同线程之间的锁争抢
         * 高并发下每分每秒都会创建大量的链接以及所属的Handler,优化后性能会有很大提升
         *
         * See <a href="https://github.com/netty/netty/issues/2289">#2289</a>.
         */
        Class<?> clazz = getClass();
        Boolean sharable = isSharableCacheMap.computeIfAbsent(
                clazz, k -> clazz.isAnnotationPresent(Sharable.class));
        return sharable;
    }
}
  • 在ChannelHandler被加入到pipeline时,会被包装成AbstractChannelHandlerContext节点加入链表。在AbstractChannelHandlerContext的构造方法中,计算出对应ChannelHandler的掩码。
  • ChannelHandler中的每个IO事件的方法都对应mask掩码的一个bit位,bit位为1则代表对该IO事件感兴趣,为0则代表不感兴趣需要跳过。在IO事件传播时,通过对应掩码进行与操作快速的判断是否需要跳过该节点。
  • 具体每一位的掩码值是通过方法上是否含有@Skip注解来判断的,带上了该注解就表示对当前IO事件不感兴趣,传播时需要跳过该ChannelHandler。
  • 掩码的计算引入了map缓存,相同类型的ChannelHandler实例的掩码不需要重复计算,在创建大量连接时,其对应pipeline中的AbstractChannelHandlerContext实例也会被大量创建,使用缓存能很好的提高性能。
  • Netty为入站,出站的ChannelHandler分别提供了ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter两个适配器,其方法中默认都带上了@Skip注解。
    在实际开发时,用户可以选择令自己的自定义ChannelHandler继承对应的Adapter,重写感兴趣的IO事件的方法。重写后的方法不会带@Skip注解,会在IO事件传播时触发其自定义的方法逻辑。

2.5 @Sharable防共享检测原理简单介绍

netty还提供了防共享检测机制,用来避免用户错误的使用共享ChannelHandler。

  • 正常情况下,每个ChannelPipeline中对应的ChannelEventHandler实例都是互相独立的,但在一些场景下使用共享的ChannelHandler能带来更好的性能。对于一些无状态的,或者架构上就是全局唯一的handler(比如dubbo中维护业务线程池的Handler),令其在不同的Channel中共享是一个好的选择。
  • netty会在ChannelHandler加入到pipeline时对其进行检查,如果存在一个ChannelHandler实例被不止一次的注册到netty中,netty会认为其被错误的注册。因为默认情况下,一个ChannelHandler实例不能同时被注册到一个以上的channel中,否则其将出现并发问题,netty会抛出异常来警告用户。
    而只有当用户在对应的ChannelHandler上显式标记上@Sharable注解,明确了其就是可以共享,已经考虑过并发的可能性时,才能在重复注册时通过校验。
  • 从个人的经历来说,我在初次使用netty时曾对@Sharable注解的功能有过误解。第一感觉是在构造流水线时,被打上了@Sharable注解的Handler会类似spring的单例模式一样,即使重复注册也会被netty自动的弄成全局唯一。
    但在了解了其工作原理后发现是反过来的,@Sharable更多的是起到一个检查的作用,避免用户错误的重复注册并发不安全的ChannelHandler。

2.6 EventLoop改造接入pipeline流水线

目前lab2版本的EventLoop还比较简单,只是在处理读事件的时候从原来的直接调用EventHandler的fireChannelRead方法,改造成了调用pipeline的fireChannelRead方法,令读事件在整个ChannelHandler流水线中传播。

java 复制代码
    private void processReadEvent(SelectionKey key) throws Exception {
        SocketChannel socketChannel = (SocketChannel)key.channel();

        // 目前所有的attachment都是MyNioChannel
        MyNioSocketChannel myNioChannel = (MyNioSocketChannel) key.attachment();

        // 简单起见,buffer不缓存,每次读事件来都新创建一个
        // 暂时也不考虑黏包/拆包场景(Netty中靠ByteToMessageDecoder解决,后续再分析其原理),理想的认为每个消息都小于1024,且每次读事件都只有一个消息
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);

        int byteRead = socketChannel.read(readBuffer);
        logger.info("processReadEvent byteRead={}",byteRead);
        if(byteRead == -1){
            // 简单起见不考虑tcp半连接的情况,返回-1直接关掉连接
            socketChannel.close();
        }else{
            // 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
            readBuffer.flip();
            // 根据缓冲区可读字节数创建字节数组
            byte[] bytes = new byte[readBuffer.remaining()];
            // 将缓冲区可读字节数组复制到新建的数组中
            readBuffer.get(bytes);

            if(myNioChannel != null) {
                // 触发pipeline的读事件
                myNioChannel.getChannelPipeline().fireChannelRead(bytes);
            }else{
                logger.error("processReadEvent attachment myNioChannel is null!");
            }
        }
    }

3.MyNettyBootstrap与新版本Echo服务器demo实现

在实现了pipeline流水线功能后,配置自定义事件处理器的方式也要有所改变,MyNetty参考netty实现了一个简单的Client/Server的Bootstrap。

其中构建pipeline的方式与netty有所不同,netty中使用了一个特殊的ChannelInboundHandler,即ChannelInitializer。ChannelInitializer会在连接被注册时触发initChannel方法,执行用户自定义的组装pipeline的逻辑,然后再将这个特殊的Handler从链表中remove掉以完成最终channel链表的构建。

而MyNetty简单起见,并没有支持用户自定义channel的惰性创建,也不支持在运行时动态的增加或删除pipeline中链表中的handler(所以没有那些handler状态的临界值判断),而是直接设计了一个MyChannelPipelineSupplier接口,在MyNIOChannel被创建时,也一并创建pipeline中的handler链表。

服务端Bootstrap
java 复制代码
public class MyNioServerBootstrap {

    private static final Logger logger = LoggerFactory.getLogger(MyNioServerBootstrap.class);

    private final InetSocketAddress endpointAddress;

    private final MyNioEventLoopGroup bossGroup;

    public MyNioServerBootstrap(InetSocketAddress endpointAddress,
                                MyChannelPipelineSupplier childChannelPipelineSupplier,
                                int bossThreads, int childThreads) {
        this.endpointAddress = endpointAddress;

        MyNioEventLoopGroup childGroup = new MyNioEventLoopGroup(childChannelPipelineSupplier,childThreads);
        this.bossGroup = new MyNioEventLoopGroup(childChannelPipelineSupplier, bossThreads, childGroup);
    }

    public void start() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);

        MyNioEventLoop myNioEventLoop = this.bossGroup.next();

        myNioEventLoop.execute(()->{
            try {
                Selector selector = myNioEventLoop.getUnwrappedSelector();
                serverSocketChannel.socket().bind(endpointAddress);
                SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
                // 监听accept事件
                selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_ACCEPT);
                logger.info("MyNioServer do start! endpointAddress={}",endpointAddress);
            } catch (IOException e) {
                logger.error("MyNioServer do bind error!",e);
            }
        });
    }
}
客户端Bootstrap
java 复制代码
public class MyNioClientBootstrap {

    private static final Logger logger = LoggerFactory.getLogger(MyNioClientBootstrap.class);

    private final InetSocketAddress remoteAddress;

    private final MyNioEventLoopGroup eventLoopGroup;

    private MyNioSocketChannel myNioSocketChannel;

    private final MyChannelPipelineSupplier myChannelPipelineSupplier;

    public MyNioClientBootstrap(InetSocketAddress remoteAddress, MyChannelPipelineSupplier myChannelPipelineSupplier) {
        this.remoteAddress = remoteAddress;

        this.eventLoopGroup = new MyNioEventLoopGroup(myChannelPipelineSupplier, 1);

        this.myChannelPipelineSupplier = myChannelPipelineSupplier;
    }

    public void start() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);

        MyNioEventLoop myNioEventLoop = this.eventLoopGroup.next();

        myNioEventLoop.execute(()->{
            try {
                Selector selector = myNioEventLoop.getUnwrappedSelector();

                myNioSocketChannel = new MyNioSocketChannel(selector,socketChannel,myChannelPipelineSupplier);

                myNioEventLoop.register(myNioSocketChannel);

                // doConnect
                // Returns: true if a connection was established,
                //          false if this channel is in non-blocking mode and the connection operation is in progress;
                if(!socketChannel.connect(remoteAddress)){
                    // 简单起见也监听READ事件,相当于netty中开启了autoRead
                    int clientInterestOps = SelectionKey.OP_CONNECT | SelectionKey.OP_READ;

                    myNioSocketChannel.getSelectionKey().interestOps(clientInterestOps);

                    // 监听connect事件
                    logger.info("MyNioClient do start! remoteAddress={}",remoteAddress);
                }else{
                    logger.info("MyNioClient do start connect error! remoteAddress={}",remoteAddress);

                    // connect操作直接失败,关闭连接
                    socketChannel.close();
                }
            } catch (IOException e) {
                logger.error("MyNioClient do connect error!",e);
            }
        });
    }
}

原来的Echo服务端/客户端demo也对逻辑进行了拆分,将业务逻辑和编解码逻辑拆分成了不同的ChannelHandler。

Echo服务器与客户端编解码处理器实现
java 复制代码
public class EchoMessageEncoder extends MyChannelEventHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(EchoMessageEncoder.class);

    @Override
    public void write(MyChannelHandlerContext ctx, Object msg) throws Exception {
        // 写事件从tail向head传播,msg一定是string类型
        String message = (String) msg;

        ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
        writeBuffer.put(message.getBytes(StandardCharsets.UTF_8));
        writeBuffer.flip();

        logger.info("EchoMessageEncoder message to byteBuffer, " +
            "message={}, writeBuffer={}",message,writeBuffer);

        ctx.write(writeBuffer);
    }
}
java 复制代码
public class EchoMessageDecoder extends MyChannelEventHandlerAdapter {

    private static final Logger logger = LoggerFactory.getLogger(EchoMessageDecoder.class);

    @Override
    public void channelRead(MyChannelHandlerContext ctx, Object msg) throws Exception {
        // 读事件从head向tail传播,msg一定是string类型
        String receivedStr = new String((byte[]) msg, StandardCharsets.UTF_8);

        logger.info("EchoMessageDecoder byteBuffer to message, " +
            "msg={}, receivedStr={}",msg,receivedStr);

        // 当前版本,不考虑黏包拆包等各种问题,decoder只负责将byte转为string
        ctx.fireChannelRead(receivedStr);
    }
}
Echo服务器与客户端demo
java 复制代码
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        MyNioServerBootstrap myNioServerBootstrap = new MyNioServerBootstrap(
            new InetSocketAddress(8080),
            // 先简单一点,只支持childEventGroup自定义配置pipeline
            new MyChannelPipelineSupplier() {
                @Override
                public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
                    MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
                    // 注册自定义的EchoServerEventHandler
                    myChannelPipeline.addLast(new EchoMessageEncoder());
                    myChannelPipeline.addLast(new EchoMessageDecoder());
                    myChannelPipeline.addLast(new EchoServerEventHandler());
                    return myChannelPipeline;
                }
            },1,5);
        myNioServerBootstrap.start();
    }
}
java 复制代码
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        MyNioClientBootstrap myNioClientBootstrap = new MyNioClientBootstrap(new InetSocketAddress(8080),new MyChannelPipelineSupplier() {
            @Override
            public MyChannelPipeline buildMyChannelPipeline(MyNioChannel myNioChannel) {
                MyChannelPipeline myChannelPipeline = new MyChannelPipeline(myNioChannel);
                // 注册自定义的EchoClientEventHandler
                myChannelPipeline.addLast(new EchoMessageEncoder());
                myChannelPipeline.addLast(new EchoMessageDecoder());
                myChannelPipeline.addLast(new EchoClientEventHandler());
                return myChannelPipeline;
            }
        });
        myNioClientBootstrap.start();
    }
}

总结

  • 在lab2中,MyNetty实现了pipeline流水线机制,允许用户构造自定义处理器链条,进行功能的解耦。同时也提供了一个Bootstrap脚手架帮助用户更快捷的实现自己的网络应用程序。
    相信在了解了MyNetty的简易版本流水线功能实现后,能帮助读者更好的理解netty中更加复杂的pipeline工作原理。
  • 目前为止,受限于MyNetty现版本的简陋功能,我们的Echo服务应用程序还非常原始,大量极端场景下的临界条件都没有处理。比如分配的ByteBuffer是固定大小,无法动态扩容,接受的消息体过大就会出错;发送和接受的消息也存在黏包、拆包的问题,等等。千里之行始于足下,在后续的lab中,MyNetty会逐步的完善上述提到的问题。
  • 在迭代MyNetty的过程中,读者也将能够体会到Netty的强大之处。因为在普通使用者无法直接感知的地方,netty底层处理了大量的边界情况,这才使得普通开发者能够基于netty高效的构建起一个健壮的网络应用程序。

博客中展示的完整代码在我的github上:https://github.com/1399852153/MyNetty (release/lab2_pipeline_handle 分支),内容如有错误,还请多多指教。