zoukankan      html  css  js  c++  java
  • Netty之ChannelHandler

    一、概述

    handler是控制socket io的各个生命周期的业务实现,netty实现了很多种协议所以有很多handler类,这儿主要关注Handler的设计、作用以及使用方法。

    二、Channel

    Channel与JDK中的Channel作用相当,是对I/O操作的封装,比如read()write()connect()close()bind()等,是Netty中核心对象。我们经常使用的NioServerSocketChannelNioSocketChannel就是其具体实现。Channel的状态改变都会触发相应事件传递到Pipeline中,被ChannelHandler处理,下面为Channel经常触发的事件:

    事件 说明
    ChannelRegistered 这里的注册强调的是Channel与EventLoop是否关联,满足下列条件才能称为registered:**1. 将相关socket注册到selector上 ;2. Channel与EventLoop关联起来 **
    ChannelActive 满足下列两个条件,就是active了:1. channel 完成了注册;2. channel 绑定到了本地端口(对NioServerSocketChannel而言)或连接到了远程主机(对NioSocketChannel而言)

    三、ChannelHandler

    ChannelHandler 是处理数据的核心对象,其对源端Channel触发的一系列事件进行处理。Netty提供了两个非常重要的子接口:

    1、ChannelInboundHandler

    ChannelInboundHandler方法如下图所示,从方法名可看出,其是对Channel触发事件的处理。

    channelRegistered

    在channel注册到线程池的时候会被触发。

    channelUnregistered

    在channel关闭的时候触发。

    channelActive

    registered完成之后且channel处理active状态,首次注册状态。主要见AbstractChannel的register0(promise)方法。

    channelnactive

    unregistered之前执行,主要见AbstractChannel的deregister方法。

    channelRead

    这个主要见pipeline的fireChannelRead方法,其被channel在获取到数据的阶段进行调用,进而触发到handler的channelRead方法。

    channelReadComplete

    这个和上面read方法一样,fireChannelReadComplete方法,被channel运行过程中read过程完成会进行调用。

    userEventTriggered

    这个也是由pipeline提供的方法作为入口fireUserEventTriggered,这个就是触发一个事件了,以IdleStateHandler为例,其一般作为心跳检测事件,放入线程池执行,判断空闲就会触发该方法,传导到各个handler。

    channelWritabilityChanged

    这个入口也在pipeline,但是好像没有怎么用到,channel并没有调用这个方法,一般也没怎么用该方法。

    exceptionCaught

    这个入口同样在pipeline,被channel的读取过程抛出异常时触发,当然不只这一个地方。

    上述方法追根溯源都是通过pipeline来触发整个执行链的,后面讲到context时会详细说明一下这个过程。只需要知道入口在这个地方即可。

    2、ChannelOutboundHandler

    ChannelOutboundHandler的方法如下图所示,都是与底层网络交互的;其实这些方法最后都会转调到Channel中定义的I/O方法上去,因为只有Channel才是与网络进行交互的。


    bind :绑定端口的操作

    connect:连接远程地址>

    disconnect:断开连接

    close:端口绑定的端口

    deregister:取消注册到线程池中

    read:设置监听读取事件

    write:写入数据的操作

    flush:刷新缓冲区,立刻发送。

    上述接口都是和连接相关的处理,而且这些都是通过pipeline的相关方法触发的,最终调用的是tail对象。实际上这个handler比较鸡肋,原因channel那节其实已经说明过,大部分实现类都做不了什么,最终都交给headContext对象调用unsafe相关方法由其处理了。大部分handler只对write方法进行了处理,其他都直接交给其他的handler处理了。我印象中之前Netty5好像要取消in和out的区别,outhandler太鸡肋了(Netty5已经放弃开发,官方的说法是forkjoin框架开发很复杂,还没准备好开发)。

    2.1 理解Inbound和Outbound:

    Inbound: 数据或事件由外向内流,比如Channel注册成功触发的注册事件;Channel读好数据后的ChannelRead事件等;或者说是与网络交互完成,接下来要由Netty处理;

    Outbound: 数据或事件由内向外流,比如Netty处理好数据后要write到网络;去网络方向read数据(read这一事件从内向外,read好数据后就是由外向内);或者说需要与网络进行交互的操作都称为Outbound;

    3、ChannelHandler与其他类之间的关系

    这个图可以看到ChannelHandler的基本结构,其有两个子类接口,ChannelInboundHandler和ChannelOutboundHandler。这也就是我之前常常提到的handler分成in和out类型,针对不同操作的方法。

    3.1 handlerAdded

    handler被添加到channel的时候执行,这个动作就是由pipeline的添加handler方法完成的。对于服务端,在客户端连接进来的时候,就通过ServerBootstrapAcceptor的read方法,为每一个channel添加了handler。该方法对于handler而言是第一个触发的方法。

    3.2 handlerRemoved

    handler被移除channel的时候执行,这个动作在pipeline中也存在,基本上是在channel断开的时候会进行这一步。

    3.3 exceptionCaught

    捕获channel生命周期中异常处理的类,该方法被移动到ChannelInboundHandler中了。已经废弃。

    注解Sharable表明该handler可以被多个pipeline重复使用。

    四、ChannelHandlerContext

    ChannelHandlerContext是ChannelHandler相互之间以及ChannelHandler与ChannelPipeline之间交互的桥梁。当创建一个Channel时,也会创建相应的ChannelHandlerContext。ChannelHandlerContext另一个重要的作用是在Pipeline中传播事件,一般和pipeline联合使用,管理handler链。handler本身没有持有顺序关系,都是通过handlerContext完成的。handlerContext自身会通过配合handler造成顺着构成的链式顺序调用下去,这里会仔细说明一下这个过程。先看中间的方法:

    接口方法就不一一介绍了,看到的都是一些熟悉的方法,针对业务的起始调用fireXXX(),其它的方法就不进行介绍了。handlerContext和pipeline一样实现类很少,基本使用的就是DefaultChannelHandlerContext,其继承自AbstractChannelHandlerContext,大部分方法也都是在抽象父类中。下面具体介绍一下是如何工作的:

    还是回到pipeline的构造handler链的过程:

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);
     
        tail = new TailContext(this);
        head = new HeadContext(this);
     
        head.next = tail;
        tail.prev = head;
    }
    

    pipeline初始化的时候构建了tail和head两个handler,这两个是特殊的,位置不能替换的。

    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
          final AbstractChannelHandlerContext newCtx;
          synchronized (this) {
              checkMultiplicity(handler);
     
              newCtx = newContext(group, filterName(name, handler), handler);
     
              addLast0(newCtx);
     
              // If the registered is false it means that the channel was not registered on an eventloop yet.
              // In this case we add the context to the pipeline and add a task that will call
              // ChannelHandler.handlerAdded(...) once the channel is registered.
              if (!registered) {
                  newCtx.setAddPending();
                  callHandlerCallbackLater(newCtx, true);
                  return this;
              }
     
              EventExecutor executor = newCtx.executor();
              if (!executor.inEventLoop()) {
                  newCtx.setAddPending();
                  executor.execute(new Runnable() {
                      @Override
                      public void run() {
                          callHandlerAdded0(newCtx);
                      }
                  });
                  return this;
              }
          }
          callHandlerAdded0(newCtx);
          return this;
      }
     
    private void addLast0(AbstractChannelHandlerContext newCtx) {
        AbstractChannelHandlerContext prev = tail.prev;
        newCtx.prev = prev;
        newCtx.next = tail;
        prev.next = newCtx;
        tail.prev = newCtx;
    }
    

    先判断这个handler有没有被其他pipeline加载过,是否是sharable类型,进行相关设置。通过handler创建context。然后插入到tail前面,后面就是添加handler之后触发的方法,触发handlerAdd方法。通过addLast0方法,可以看见context中的参数prev和next被初始化了。也就是说通过当前的context能够找到前一个和后一个context了。下面抽出handler接口的其中一个方法,我们来研究一下handler链是怎么执行的。

     public ChannelHandlerContext fireChannelActive() {
          invokeChannelActive(findContextInbound());
          return this;
      }
     
      static void invokeChannelActive(final AbstractChannelHandlerContext next) {
          EventExecutor executor = next.executor();
          if (executor.inEventLoop()) {
              next.invokeChannelActive();
          } else {
              executor.execute(new Runnable() {
                  @Override
                  public void run() {
                      next.invokeChannelActive();
                  }
              });
          }
      }
     
      private void invokeChannelActive() {
          if (invokeHandler()) {
              try {
                  ((ChannelInboundHandler) handler()).channelActive(this);
              } catch (Throwable t) {
                  notifyHandlerException(t);
              }
          } else {
              fireChannelActive();
          }
      }
     
    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }
    

    handler的任何一个方法,在context中都是由三个这样的方法来构成链式执行的。static方法都是给pipeline调用的,其入参就是head,handler链的头结点。调用了invokeChannelActive()方法,这里就是我们的handler方法相关内容被触发了。乍一看好像执行完了就没了啊,这个地方要接下去就需要handler配合了。随便找一个handler,比如ChannelInboundHandlerAdapter,其相关方法都调用了ctx.fireChannelActive(); 这里就接上了,ctx的fireChannelActive不就是回到了invokeChannelActive()方法了。其入参就不再是当前的context,而是通过findContextInbound来找到下一个in类型的handler,这也说明这个接口是针对in类型的接口。所以一个基本的循环就是ctx.invokeChannelActive(ctx) ->ctx.invokeChannelActive() -> hander.channelActive()->ctx.fireChannelActive(ctx)->findContextInbound()->ctx.invokeChannelActive(ctx)。这么个循环链,起点就是pipeline的invokeChannelActive(head),终点就是handler.channelActive()没有调用ctx.channel的时候,最后的tailContext就是没有执行任何操作,所以执行到这链路就断了。

    五、ChannelPipeline

    ChannelPipeline是一系列ChannelHandler的组合,与Channel相关的所有数据和事件都会流经相应的ChannelPipeline进行处理。ChannelPipeline也可以触发事件传播,与ChannelHandlerContext触发不同的是,ChannelPipeline触发的事件都是从第一个ChannelHandler开始处理,而ChannelHandlerContext触发的事件总是从下一个ChannelHandlerContext开始处理

    Pipeline的事件流

    当事件流在ChannelPipeline中流动时,可以调用ChannelHandlerContext或ChannelPipeline的Outbound方法改变事件的流向

    在程序中,要手动触发Inbound到Outbound方向的转变;举个例子,比如我们定义了若干个InboundHandler,处理了用户数据后,返回给用户一个Success,那么必须在某个InboundHandler里调用ChannelHandlerContext.write()方法让数据流改变方向,否则,永远都无法给用户返回;若不手动改变方向,输入数据最终会走到TailContext(默认最后一个InboundHandler)中,而这个Handler几乎什么都没做,那么OutBound方法永远都等不到被调用

    六、事件流

    无论是NioSocketChannel还是NioServerSocketChannel,Channel的事件都是按照下图所示流转。

    七、总结

    本节对handler的一个基本内容进行了说明,主要讲解了handlerContext的执行过程,明确了调用链的入口在于pipeline,通过pipeline和context的控制,保证链的执行。channel的生命周期控制pipeline就能控制整个handler的执行。下图给个具体说明:

    八、站在巨人的肩膀上

    1、Netty Handler

  • 相关阅读:
    ZOJ 1002 Fire Net (火力网)
    UVa OJ 117 The Postal Worker Rings Once (让邮差只走一圈)
    UVa OJ 118 Mutant Flatworld Explorers (变体扁平世界探索器)
    UVa OJ 103 Stacking Boxes (嵌套盒子)
    UVa OJ 110 MetaLoopless Sorts (无循环元排序)
    第一次遇到使用NSNull的场景
    NSURL使用浅析
    从CNTV下载《小小智慧树》
    NSDictionary and NSMutableDictionary
    Category in static library
  • 原文地址:https://www.cnblogs.com/Courage129/p/14252046.html
Copyright © 2011-2022 走看看