• netty 之Channel、pipeline,ChannelHandler和ChannelHandlerContext之疑难问题 碎碎念2


    先引用 https://www.jianshu.com/p/039fb90fa5bf  做个小结:

    在Netty上,Pipeline把ChannelHandler串联在一起来组织处理逻辑。比如实现协议栈HTTP,HTTP2。而ChannelHandlerContext可以认为是Pipeline用于串联ChannelHandler的纽带。开发者的业务逻辑基本上是在ChannelHandler实现的,理解这三者以及三者之间的关联是使用Netty构建模块化、可复用程序的关键。
    上图是ChannelHandler的类结构图,在此总结下
     
    ChannelHandler的功能:
    • 响应ChannelHandler状态变化
    • 响应与其关联的Channel生命周期内的状态变化以及处理接收的数据
    • 处理与其关联的Channel上的 Outbound 操作
    ChannelHandlerAdapter 提供了所有各种类型 Handler 都需要的通用操作。是ChannelOutboundHandlerAdapter、 ChannelInboundHandlerAdapter 都需要继承的,因为 他们都能够处理 isSharable、 added、removed 、exceptionCaught
     
    ChannelOutboundHandler
    ChannelOutboundHandler 给用户机会来进一步处理用户在Channel上的操作。一些比较典型的应用场景是filter掉一些操作,即根据需求拒绝一些操作;Socket面向字节流,在实现具体 的协议比如Http时,可以让用户只处理自己关心的数据,OutboundHandler可以把这些数据封装成协议需要的数据,然后交给socket发送。
     
    ...
     
    可以概括Socket 和 ChannelPipeline以及ChannelHandler和ChannelPipeline之间的关系。
    • ChannelPipeline把ChannelHandler串联在一起来拦截处理Channel产生的inbound和outbound事件,这些ChannelHandler构成了应用程序的数据和事件处理逻辑。
    • 每个Channel会与唯一一个ChannelPipeline实例进行绑定。
    • inbound事件的流向Head -> Tail,
    • outbound事件的流向是Tail->Head,这有点像网络协议栈,而且Netty本身实现的协议比如Http,Http2都是基于ChannelHandler,在其上实现数据的解码与编码。我的理解是ChannelPipeline把用户的逻辑同socket关联起来,我觉着其上面最适合实现的逻辑应该是协议的实现,至于用户的业务逻辑不应该在ChannelHandler上实现。

    再谈出站和入站

    官方原图:

    这个图 其实是没错的,不过,当然需要说明一下。 Netty Internal I/O Threads 是 底层传输层,是head , 是NIO , 是socket 部分。  I/O Request是tail ,是用户发出的 读写。 从下到上是 入站,反之是出站。
     
    但是还不够清晰啊  而且 head、 tail 都没有画出来啊,各个 Inbound 、 Outbound Handler 1/2/3/4 的数字编号是怎么来的? 其实就是从pipeline的 head 到 tail ,Handler 的编号依次增长,Inbound 、Outbound 都是如此.
     
    socket 的read方法 表示入站,从 head 到 tail ,从而数字编号 是递增的。而write方法表示出站,从 tail 到 head ,从而数字编号 是递减的 ...
     
     
    为什么入站,是从 head 到 tail ? 构建pipeline的时候,先添加的就是靠近head,后面添加的就是靠近tail。head 是靠近 socket的,入站从socket 开始,也就是从 head开始。tail。
     
    很难记住? 哦,只要记住 head 就是socket ,然后稍微推理一下, 就好了。从而, 对于 出站和入站事件,都是沿着pipeline ”下一个“ 的反向传播, 不过,方向是相反的。一个是 从。
     
    他们对于 ”下一个“ 的解释不一样。
     
    从源码看:
    ctx.channel().pipeline().fireChannelRead(msg);
    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
      AbstractChannelHandlerContext.invokeChannelRead(head, msg); // 可见是 head
      return this;
    }
     可见是读事件是从 head 开始传播。
     
    ctx.channel().pipeline().write() 是
    @Override
    public final ChannelFuture write(Object msg) {
      return tail.write(msg);
    }
     可见是读事件是从 tail 开始传播。
     
    需要注意的是,head 永远是head,addFirst也不会改变head,而是head之后:
    private void addFirst0(AbstractChannelHandlerContext newCtx) {
        AbstractChannelHandlerContext nextCtx = head.next;
        newCtx.prev = head;
        newCtx.next = nextCtx;
        head.next = newCtx; //  添加到 head 之后。
        nextCtx.prev = newCtx;
    }

     head ? tail? 命名其实不太好, 不直观啊! 不如, local、remote,

    ctx.xxx(..);和ctx.channel().pipeline().xxx(..);  是什么区别?

    比如
    ctx.fireChannelRead(msg);和ctx.channel().pipeline().fireChannelRead(msg);
     
     
    io.netty.channel.DefaultChannelPipeline#fireChannelRead 是从头 开始读, 所以如果在channelRead方法调用它的话,可能出现重复读。
     
      @Override
        public final ChannelPipeline fireChannelRead(Object msg) {
            AbstractChannelHandlerContext.invokeChannelRead(head, msg);
            return this;
        }
    而 io.netty.channel.AbstractChannelHandlerContext#fireChannelRead是从当前处理器开始,通过findContextInbound沿着pipeline入站方向找到next
    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
       invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg); //从当前处理器开始,通过findContextInbound沿着pipeline入站方向找到next
       return this;
    }
    
    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }
    其实理解了ctx和channel、pipeline的关系就好。实际上调用channel、pipeline方法入站的时候, 一定是从入站的起点到入站的的终点,沿着pipeline传播。出站的时候类似。
    当然,入站的起点其实就是head,和出站的起点 刚好是相反的。
     
    另外发现,channel pipeline 之中有很多方法名字、参数基本一样,其实channel().xxx(..) 就是调用了 pipeline().xxx(..)
     
    其实呢, 只要记住,调用 ctx 的方法, 意思就是 传播。 没有别的意思, 不调用就不会传播。 调用handler的 super 的方法也是如此!!。

    ---

    错误使用 pipeline() 方法导致的死循环

    之前就遇到一个 错误使用 pipeline() 方法导致的死循环问题,如下:
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
      System.out.println(integer.incrementAndGet() + " " + ctx.handler());
     
      ...
      ctx.pipeline().fireChannelRead(msg ); // 第 388 次的时候: java.lang.StackOverflowError
    }
     
    自定义的 ChannelInboundHandlerAdapter 里面 的channelRead方法,又调用ctx.pipeline().fireChannelRead, 然后又从头开始传递读事件,然后又到了这个方法, 如此往返。这就导致了调用的死循环,从而导致在第 388 次的时候出现StackOverflowError , 因为死循环导致的线程栈实在太深了。
     
    同样的,如果自定义的 ChannelOutboundHandlerAdapter 里面 的write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)方法,又调用了ctx.pipeline().write, 那么也会死循环
     
      

    channelRead 和 channelReadComplete什么区别,何时使用?

    什么时候被调用?
    区别在于,
     

    参考 https://segmentfault.com/q/1010000018753423 可以得到一些答案

    我的理解是,对端一次写事件,可能引起本端的一次读事件,也可能有多个channelRead ,但是只会有一个channelReadComplete

    why ?

    因为,读 可能一次性读很多数据, 很多个 buffer,然后每个一个bugger 就是 调用一次 channelRead 调用。 全部buffer 读完了, 然后才是 channelReadComplete

    到底write writeAndFlush区别是什么

    有时候你会发现 write 竟然写不出去, 莫非夜路走多了遇到了鬼?

    其实是这样的,两者有一些区别,前者 不一定写出去, 多个 write 可能在不确定的某一时刻写出去, 后者可以确保缓冲区的内容全部写出去!

    write 是写到 写缓存buffer中, 不一定直接写出去, 需要等待缓存满了才发出去,—— 注意, 如果缓存满了, 那么之前的数据, 会全部一并 写出去! 而不是。

    而 writeAndFlush 是直接写出去!! 而且是 把 之前的数据, 会全部一并 写出去!


    就是说,如果有多个 write , 每次写很少数据, 然后一个writeAndFlush , 那么就一起写出去了!

    这个时候, 如果多次write 被一次性读到, 那么可能就是 数据连在了一起, 没有换行.. 就可能不是很 直观。 如果分多次读到,那么自然就不会这样啦。。

    到底read出站事件方法意味着什么?

    同样是出站 write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) 有ctx 、msg、promise 等参数, 为什么

    ChannelOutboundHandler  的read方法 就只有一个参数:

    public void read(ChannelHandlerContext ctx) throws Exception {... }

    那么, 到底ChannelOutboundHandler 的read出站事件方法意味着什么?什么时候被调用?

    观察发现, 事件是这样触发的, 从head 开始:

    head. channelActive or channelReadComplete --> readIfIsAutoRead --> channel.read(); --> pipeline.read(); --> pipeline.read(); -- > tail.read();


    也就是 通道 激活之后, 才开始准备读数据! 或者 一次读完毕之后才。。


    真的是 绕来绕去啊!

    简单说,就是 只要读事件完毕, 就触发read,作为出站事件,从管道的 tail 传播。

     
     head 中:
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
      ctx.fireChannelActive();
      readIfIsAutoRead();
    }
     
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
      ctx.fireChannelReadComplete();
      readIfIsAutoRead();
    }
     
     
     
    private void readIfIsAutoRead() {
      if (channel.config().isAutoRead()) {
        channel.read();
      }
    }
    .
     
    io.netty.channel.AbstractChannel#read : 
    @Override
    public Channel read() {
      pipeline.read();
      return this;
    }
     
     
    @Override
    public final ChannelPipeline read() {
      tail.read();
      return this;
    }
     
     
    abstract void init(Channel channel) throws Exception;
     
      
    不过呢, HeadContext 是双向的处理器,它也有 read 方法:
    @Override
    public void read(ChannelHandlerContext ctx) {
      unsafe.beginRead();
    }
     
    tail 的read 最后 又传递到了head !! 神奇啊!!
     
    unsafe.beginRead(); 是什么?
     
    委托到了 NioByteUnsafe
     
    @Override public final void beginRead() { doBeginRead(); }
     
    AbstractNioChannel 中
    @Override
    protected void doBeginRead() throws Exception {
        // Channel.read() or ChannelHandlerContext.read() was called
        final SelectionKey selectionKey = this.selectionKey;
        if (!selectionKey.isValid()) {
            return;
        }
    
        readPending = true;
    
        final int interestOps = selectionKey.interestOps();
        if ((interestOps & readInterestOp) == 0) {
            selectionKey.interestOps(interestOps | readInterestOp);
        }
    }
    doBeginRead() 做的事情很简单,拿到处理过的selectionKey,然后如果发现该selectionKey若在某个地方被移除了readInterestOp操作,这里给他加上,事实上,标准的netty程序是不会走到这一行的,只有在三次握手成功之后,如下方法被调用
    原来就是 重新注册 读监听器到 selectionKey, 等待新的读事件的到来。
     
    也就是 处理完, 然后又再次注册,然后 等待远端的数据过来,然后再次读。。 如此反复循环。
     
    可以说,这个过程是 被保证是 有序的, 只有前面的数据处理完了,即channelReadComplete,然后 底层的nio 处理器才会继续从远端获取数据。—— 中间可能有 异步阻塞、拥塞控制机制。
     
     
    如果我们 的出站处理器, 实现了read 方法,然后 不调用 super.read 或者 ctx.read的话, 循环就会被中断, 无法继续接收到数据了!!
     
    当然,如果注册没有被取消, 其实read 方法被中断了也不要紧! 否则..
     
    所以, 重写read 方法的时候, 必须要慎重! 否则导致奇怪问题!。
     
     
     
     


    版权声明
    本文原创发表于 博客园,作者为 阿K .     本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则视为侵权。
    欢迎关注本人微信公众号:觉醒的码农,或者扫码进群:

  • 相关阅读:
    如何更改 iOS 和安卓浏览器上的 input[type="radio"] 元素的默认样式?
    js获取样式、currentStyle和getComputedStyle的兼容写法
    js选中文字兼容性解决
    最短的IE判断var ie=!-[1,]分析
    总结oninput、onchange与onpropertychange事件的用法和区别
    jQuery工具函数
    Vnpy官网汇总
    Anaconda下各个Python版本下载地址
    APScheduler——定时任务框架
    PyQt5——隐藏控件并保留位置
  • 原文地址:https://www.cnblogs.com/FlyAway2013/p/14952753.html
Copyright © 2020-2023  润新知