Java Netty 学习(九) - ChannelPipeline

上一篇文章学习了Channel,它屏蔽了许多底层的java.net.Socket的操作,
那么,当有了数据流之后,就到了如何处理它的时候,那么本篇文章先看ChannelPipeline和ChannelHandler。

概念

Netty里面的ChannelPipeline就类似于一根管道,而ChannelHandler类似于里面的拦截器,当一个拦截器拦截完后,可以向后传递,或者跳过。
ChannelPipeline提供了提供了ChannelHandler链上的容器,并定义了用于该链的传播入站和出站的流API。ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制

很简单的理解就是编码器和解码器都是ChannelHandler,当数据在网络上传输时候,通信双方会定义协议和格式,此时到了Channel端,则需要进行解码,从而再进行业务处理,而处理完,再进行编码发送出去。当然这是例子,实际中可以定一个多个ChannelHandler,这些ChannelHandler将以链表的形式存在,再进行事件传递。

当创建一个新的Channel时,都会分配了一个新的ChannelPipeline,该关联是永久的,该通道既不能附加另一个ChannelPipeline也不能分离当前的ChannelPipeline
下面分别介绍ChannelPipline

ChannelPipline

ChannelPipline可以理解为管家,对ChannelHandler进行拦截和调度。
当一个消息被ChannelPipeline的Handler链拦截和处理过程是怎样的呢?
在上文中的HelloClientHandlerchannelActive打一个端点,分析其执行流程
先来分析下:

  1. 启动Server.java,随后启动Client.java
  2. 当Client尝试连接到Server时,初始化了Channel信息,可看:Netty的Channel
  3. 随后,由于HelloClientHandler也是一个Handler,它的调用必定经过ChannelPipeline。
  4. 随后,底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件
  5. 由IO线程EventLoopGroup 分配线程作为Selector,等待到了事件变化,并将事件按照类别处理,如下:
    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();     //获取channel的unsafe内部类
        if (!k.isValid()) {             //key可用
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop();            //从Channel中获取对应的EventLoop
            } catch (Throwable ignored) {
            // 如果抛出异常则直接返回,因为原因可能是还没有EventLoop
                return;
            }
            // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
            // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
            // still healthy and should not be closed.
            // See https://github.com/netty/netty/issues/5125
            if (eventLoop != this || eventLoop == null) {
                return;
            }
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();    // 获取Selector的keys
            // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
            // the NIO JDK channel implementation may throw a NotYetConnectedException.
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {  // 可读并且连接的事件
                // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
                // See https://github.com/netty/netty/issues/924
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();  //先调用finishConnect,否则jdk会抛出NotYetConnectedException
            }

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {  //可读并且写事件
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                ch.unsafe().forceFlush();
            }

            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();   //读事件
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }
  1. 此时由于NIO的Keys变化,执行了unsaferead()方法,而在read方法里面,将调用pipelinefireChannelRead方法,将事件流传递,如read中下面代码:
				int size = readBuf.size();
                for (int i = 0; i < size; i ++) {
                    readPending = false;
                    pipeline.fireChannelRead(readBuf.get(i));
                }
  1. 此时消息已经传递到ChannelPipeline,通过fireChannel*方法将消息向后传递向后传递,利用ChannelHandlerContext来依次传递给channelHandler1,channelHandler2,channelHandler3…

整个read事件就如上。
那么write事件呢?可以理解为和read相反,消息从tailHandler开始,途经channelHandlerN……channelHandler1, 最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回等。

事件

事件主要分为inbound和outbound,即入站和出站事件
fireChannelActivefireChannelReadfireChannelReadComplete等则为入站事件,而
writeflushdisconnect则为出战事件。
看ChannelInbound和ChannelOutbound结构图:
Java Netty 学习(九) - ChannelPipeline_第1张图片

Java Netty 学习(九) - ChannelPipeline_第2张图片

即一般实现ChannelHandler,继承相应的装饰器类Adapter即可,然后重写需要的方法。

构建ChannelPipeline

ChannelPipline接口提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出战的事件流API。当Channel被创建时, 它会自动的分配到它专属的ChannelPipline中。
而且并不用程序员去创建一个ChannelPipline,只需要往这个容器中丢东西就好了,记得在BootStrap启动时:

pipeline = ch.pipeline();
pipeline.addLast("decoder", new MyProtocolDecoder());
pipeline.addLast(new HelloClientHandler());
pipeline.addLast("encoder", new MyProtocolEncoder());

ChannelPipeline的机制和和Map很相似,以简直对的方式讲ChannelHandler管理起来,增删改查,但是此时会有问题,类似与Map有ConcurrentHashMap一类并发容器,理论上,ChannelPipeline会有IO线程和用户线程之间的并发情况,以及用户之间的并发情况,那么ChannelPipeline并发下怎么解决呢?
在ChannelPipeline中有四个方法:

  • addFirst
  • addBefore
  • addAfter
  • addLast
    通过查DefaultChannelPipeline的源码不难发现,这四个方法都使用Synchronized(this)来加锁,将当前整个ChannelPipeline给锁起来,这样依赖就很好的避免了更改Pipeline内部链表结构时候出现的并发问题。
    每当添加时候,ChannelPipeline都会调用checkDuplicateName(name);进行同名校验,从表头循环到表尾进行校验:
    private AbstractChannelHandlerContext context0(String name) {
        AbstractChannelHandlerContext context = head.next;
        while (context != tail) {
            if (context.name().equals(name)) {
                return context;
            }
            context = context.next;
        }
        return null;
    }

DefaultChannelPipeline 是ChannelPipline的默认实现,能够满足大多数ChannelPipline的需求,其父类实现了ChannelInboundInvokerChannelOutboundInvoker 用于能够分别给不同类型的事件发送通知。
Java Netty 学习(九) - ChannelPipeline_第3张图片

ChannelPipeline中的耗时操作

ChannelPipline中的每一个ChannelHandler都是通过它的EventLoop(IO线程)来处理它的事件的。所以不要阻塞这个线程,因为会对整体 IO产生负面影响。
但有时可能需要与那些使用阻塞API的遗留代码进行交互,对于这种情况,ChannelPipeline有一些接受一个EventExecutorGroupaddFirstaddLastaddBeforeaddAfter 方法,如果一个事件被传递给一个自定义的EventExecutorGroup,它将被包含在这个EventExecutorGroupEventExecutor所处理 (类似新开一个线程),从而被该Channel本身的EventLoop中移除,对于这种用例,Netty提供一个交DefaultEventExecutorGroup默认实现
当然,在上述四个add*方法也是有Synchronized修饰的

ChannelHandlerContext接口

  • ChannelHandlerContext代表ChannelHandlerChannelPipline之间的关联,每当有ChannelHandler添加到ChannelPipline中时,
    都会创建ChannelHandlerContext,它的主要功能是管理它所关联的ChannelHandler和它同一个ChannelPipline中其他ChannelHandler
    之间的交互。
  • ChannelHandlerContext有很多方法,其中一些方法也存在于ChannelChannelPipline本身上,但有一点重要不同,如果调用Channel
    或者ChannelPipline上的这些方法,它们将沿着整个ChannelPipline进行传播。而调用位于ChannelHandlerContext上相同方法,
    则讲从当前所关联的ChannelHandler开始,并且只会传播给位于该ChannelPipline中下一个能够处理的ChannelHandler
  • ChannelHandlerContextChannelHandler之间的关联是永远不变的,所以缓存你对他的引用是安全的
  • 相对于其他类的同名方法,ChannelHandlerContext的方法将产生更短的事件流,应该尽可能利用这个特性来获得最大的性能
    虽然被调用的CHannel或者ChannelPipline上的write方法一直传播事件通过整个ChannelPipline,但是在ChannelHandler的级别上,
    事件从一个ChannelHandler到下一个ChannelHandler的移动是由ChannelHandlerContext上调用完成的。

一个ChannelHandler可以从属于多个ChannelPipline,所以它也可以绑定到多个ChannelHandlerContext实例,对于这种用法,
对应的ChannelHandler必须要使用@Shareable注解标注,否则试图将它添加多个ChannelPIpline将会触发异常。显而易见,为了安全的
备用与多个并发的Channel,这样的ChannelHandler必须是线程安全的。

参考资料:

  1. Netty In Action
  2. Netty 权威指南
  3. Netty 源码 4.1.12 Final

你可能感兴趣的:(Java-Netty)