Netty源码篇8-Pipeline Handler HandlerContext创建和调度 handler源码剖析

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

Netty 中ChannelPipeline、ChannelHandler、ChannelHandlerContext是非常核心的组件,我们从源码来分析netty师如何设计这三个核心组件的,并分析师如何创建和协调工作的。

1. ChannelPipeline | ChannelHandler | ChannelHandlerContext

1.1 介绍

1.1.1 三者关系

  • 每当 ServerSocket 创建一个新的连接,就会创建一个 Socket,对应的就是目标客户端。
  • 每一个新创建的 Socket 都将会分配一个全新的 ChannelPipeline(以下简称 pipeline)
  • 每一个 ChannelPipeline 内部都含有多个 ChannelHandlerContext(以下简称 Context)
  • 他们一起组成了双向链表,这些 Context 用于包装我们调用 addLast 方法时添加的 ChannelHandler(以下简称handler):

image.png

  • 说明:
    • ChannelSocket 和 ChannelPipeline 是一对一的关联关系 ,而 pipeline 内部的多个 Context 形成了链表,Context 对只是对 Handler 的封装
    • 当一个请求进来的时候,会进入 Socket 对应的 pipeline,并经过 pipeline 所有的 handler,对,就是设计模式中的过滤器模式。

1.1.2 ChannelPipeline 作用和设计

pipeline 的接口设计:

image.png

image.png

可以看到该接口继承了 inBound,outBound,Iterable 接口,表示他可以调用数据出站的方法和入站的方法,同时也能遍历内部的链表,看看它几个代表性的方法,基本上都是针对 handler 链表的插入,追加,删除,替换操作,类似是一个 LinkedList。同时,也能返回 channel(也就是 socket):

image.png

说明:

  • 这是一个 handler 的 list,handler 用于处理或拦截入站事件和出站事件,pipeline 实现了过滤器的高级形式,以便用户控制事件如何处理以及 handler 在 pipeline 中如何交互。

  • 上图描述了一个典型的 handler 在 pipeline 中处理 I/O 事件的方式,IO 事件由 inboundHandler 或者 outBoundHandler 处理,并通过调用 ChannelHandlerContext.fireChannelRead 方法转发给其最近的处理程序 。

  • 入站事件由入站处理程序以自下而上的方向处理,如图所示。入站处理程序通常处理由图底部的 I / O 线程生成入站数据。入站数据通常从如 SocketChannel.read(ByteBuffer) 获取。

  • 通常一个 pipeline 个 有多个 handler,例如,一个典型的服务器在每个通道的管道中都会有以下处理程序:

    • 协议解码器 - 将二进制数据转换为 Java 对象。
    • 协议编码器 - 将 Java 对象转换为二进制数据。
    • 业务逻辑处理程序 - 执行实际业务逻辑(例如数据库访问)
  • 你的业务程序不能将线程阻塞,会影响 IO 的速度,进而影响整个 Netty 程序的性能。如果你的业务程序很快,就可以放在 IO 线程中,反之,你需要异步执行。或者在添加 handler 的时候添加一个线程池,例如:

    // 下面这个任务执行的时候,将不会阻塞 IO 线程,执行的线程来自 group 线程池
    pipeline.addLast(group,“handler”,new MyBusinessLogicHandler());
    复制代码

1.1.3 ChannelHandler 作用和设计

源码:

public interface ChannelHandler {
    //当把 ChannelHandler 添加到 pipeline 时被调用
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;
    //当从 pipeline 中移除时调用
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
    // 当处理过程中在 pipeline 发生异常时调用
    @Deprecated
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
复制代码

ChannelHandler的作用就是处理IO事件或拦截IO事件,并将其转发给下一个处理程序ChannelHandler。

Handler 处理事件时分入站和出站的,两个方向的操作都是不同的,因此,Netty 定义了两个子接口继承ChannelHandler:

  • ChannelInboundHandler 入站事件接口 image.png

    • channelActive 用于当 Channel 处于活动状态时被调用;
    • channelRead 当从 Channel 读取数据时被调用等等方法。
    • 程序员需要重写一些方法,当发生关注的事件,需要在方法中实现我们的业务逻辑,因为当事件发生时,Netty 会回调对应的方法。
  • ChannelOutboundHandler 出站事件接口

    image.png

    • bind 方法,当请求将 Channel 绑定到本地地址时调用
    • close 方法,当请求关闭 Channel 时调用等等
    • 出站操作都是一些连接和写出数据类似的方法。
  • ChannelDuplexHandler 处理出站和入站事件

    image.png

    • ChannelDuplexHandler 间接实现了入站接口并直接实现了出站接口。是一个通用的能够同时处理入站事件和出站事件的类。------尽量少用

1.1.4 ChannelHandlerContext 作用和设计

ChannelHandlerContext UML 图:

image.png

ChannelHandlerContext 继承了出站方法调用接口和入站方法调用接口。ChannelOutboundInvoker和ChannelInboundIncoker部分源码:

image.png image.png

这两个 invoker 就是针对入站或出站方法来的,就是在入站或出站 handler 的外层再包装一层,达到在方法前后拦截并做一些特定操作的目的

ChannelHandlerContext 部分源码:

image.png

  • ChannelHandlerContext 不仅仅时继承了他们两个的方法,同时也定义了一些自己的方法
  • 这些方法能够获取 Context 上下文环境中对应的比如 channel,executor,handler ,pipeline,内存分配器,关联的 handler 是否被删除。
  • Context 就是包装了 handler 相关的一切,以方便 Context 可以在 pipeline 方便的操作 handler

1.2. ChannelPipeline | ChannelHandler | ChannelHandlerContext 创建过程

分为 3 个步骤来看创建的过程:

  • 任何一个 ChannelSocket 创建的同时都会创建 一个 pipeline。
  • 当用户或系统内部调用 pipeline 的 add*** 方法添加 handler 时,都会创建一个包装这 handler 的 Context。
  • 这些 Context 在 pipeline 中组成了双向链表。

1.2.1 Socket 创建的时候创建 pipeline

在 SocketChannel 的抽象父类 AbstractChannel 的构造方法中

protected AbstractChannel(Channel parent) {
    this.parent = parent; //断点测试
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}
复制代码

Debug 一下, 可以看到代码会执行到这里, 然后继续追踪到

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;
}
复制代码

说明:

  • 将 channel 赋值给 channel 字段,用于 pipeline操作channel。

  • 创建一个 future 和 promise,用于异步回调使用。

  • 创建一个 inbound 类型又是 outbound 的 tailContext,创建一个既是 inbound 类型又是 outbound 类型的 headContext.

  • 最后,将两个 Context 互相连接,形成双向链表。

  • tailContext 和 HeadContext 非常的重要,所有 pipeline 中的事件都会流经他们。

1.2.2 在 add** 添加处理器的时候创建 Context**

看下 DefaultChannelPipeline 的 addLast 方法如何创建的 Context,代码如下

@Override
public final ChannelPipeline addLast(String name, ChannelHandler handler) {
    return addLast(null, name, handler);
}
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    synchronized (this) {
        checkMultiplicity(handler);
        // 创建AbstractChannelHandlerContext对象
        newCtx = newContext(group, filterName(name, handler), handler);
        // 添加
        addLast0(newCtx);
        ...
        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }
    // 最后,同步或者异步或者晚点异步的调用 callHandlerAdded0 方法
    callHandlerAdded0(newCtx);
    return this;
}
复制代码

说明:

  • pipeline 添加 handler,参数是线程池,name 是 null, handler 是我们或者系统传入的 handler。Netty 为了防止多个线程导致安全问题,同步了这段代码,步骤如下:
  • 检查这个 handler 实例是否是共享的,如果不是,并且已经被别的 pipeline 使用了,则抛出异常。
  • 调用 newContext(group, filterName(name, handler), handler) 方法,创建一个 Context。从这里可以看出来,每次添加一个 handler都会创建一个关联 Context。
  • 调用 addLast 将 方法,将 Context 追加到链表中。
  • 如果这个通道还没有注册到 selecor 上,就将这个 Context 添加到这个 pipeline 的待办任务中。当注册好了以后,就会调用 callHandlerAdded0 方法(默认是什么都不做,用户可以实现这个方法)。
  • 到这里,针对三对象创建过程,了解的差不多了,和最初说的一样,每当创建 ChannelSocket 的时候都会创建一个绑定的 pipeline,一对一的关系,创建 pipeline 的时候也会创建 tail 节点和 head 节点,形成最初的链表。tail是入站 inbound 类型的 handler, head 既是 inbound 也是 outbound 类型的 handler。在调用 pipeline 的 addLast方法的时候,会根据给定的 handler 创建一个 Context,然后,将这个 Context 插入到链表的尾端(tail 前面)。

1.3 Pipeline Handler HandlerContext 创建过程梳理

  • 每当创建 ChannelSocket 的时候都会创建一个绑定的 pipeline,一对一的关系,创建 pipeline 的时候也会创建tail 节点和 head 节点,形成最初的链表。

  • 在调用 pipeline 的 addLast 方法的时候,会根据给定的 handler 创建一个 Context,然后,将这个 Context 插入到链表的尾端(tail 前面)。

  • Context 包装 handler,多个 Context 在 pipeline 中形成了双向链表

  • 入站方向叫 inbound,由 head 节点开始,出站方法叫 outbound ,由 tail 节点开始

2. ChannelPipeline 调度 handler 的源码剖析

当一个请求进来的时候,ChannelPipeline是如何调用内部的这些handler?首先,当一个请求进来的时候,会第一个调用pipeline的相关方法,如果是入站事件,这些方法由fire开头,表示开始管道的流动。让后面的handler继续处理。

2.1 说明

public final ChannelPipeline fireChannelActive() {
   AbstractChannelHandlerContext.invokeChannelActive(head); //断点
   return this;
}
复制代码

DefaultChannelPipeline是如何实现这些fire方法?

2.2 DefaultChannelPipeline 源码

public class DefaultChannelPipeline implements ChannelPipeline {
    @Override
    public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }
    @Override
    public final ChannelPipeline fireChannelInactive() {
        AbstractChannelHandlerContext.invokeChannelInactive(head);
        return this;
    }
    @Override
    public final ChannelPipeline fireExceptionCaught(Throwable cause) {
        AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
        return this;
    }
    @Override
    public final ChannelPipeline fireUserEventTriggered(Object event) {
        AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
        return this;
    }
    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
        AbstractChannelHandlerContext.invokeChannelRead(head, msg);
        return this;
    }
    @Override
    public final ChannelPipeline fireChannelReadComplete() {
        AbstractChannelHandlerContext.invokeChannelReadComplete(head);
        return this;
    }
    @Override
    public final ChannelPipeline fireChannelWritabilityChanged() {
        AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
        return this;
    }
}
复制代码

说明:可以看出来,这些方法都是 inbound 的方法,也就是入站事件,调用静态方法传入的也是 inbound 的类型 head handler。用这些静态方法则会调用 head 的 的 ChannelInboundInvoker 用 接口的方法,再然后调用 handler 的真正方法

2.3 piepline 的 outbound 的 fire 方法实现

public class DefaultChannelPipeline implements ChannelPipeline {
    @Override
    public final ChannelFuture bind(SocketAddress localAddress) {
        return tail.bind(localAddress);
    }
    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress) {
        return tail.connect(remoteAddress);
    }
    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        return tail.connect(remoteAddress, localAddress);
    }
    @Override
    public final ChannelFuture disconnect() {
        return tail.disconnect();
    }
    @Override
    public final ChannelFuture close() {
        return tail.close();
    }
    @Override
    public final ChannelFuture deregister() {
        return tail.deregister();
    }
    @Override
    public final ChannelPipeline flush() {
        tail.flush();
        return this;
    }
    @Override
    public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
        return tail.bind(localAddress, promise);
    }
    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }
    @Override
    public final ChannelFuture connect(
        SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, localAddress, promise);
    }
    @Override
    public final ChannelFuture disconnect(ChannelPromise promise) {
        return tail.disconnect(promise);
    }
}
复制代码

说明:

  • 这些都是出站的实现,但是调用的是 outbound 类型的 tail handler 来进行处理,因为这些都是 outbound 事件。
  • 出站是 tail 开始,入站从 head 开始。因为出站是从内部向外面写,从 tail 开始,能够让前面的 handler 进行处理,防止 handler 被遗漏,比如编码。反之,入站当然是从 head 往内部输入,让后面的 handler 能够处理这些输入的数据。比如解码。因此虽然 head 也实现了 outbound 接口,但不是从 head 开始执行出站任务

2.4 如何调度

image.png

说明:

  • pipeline 首先会调用 Context 的静态方法 fireXXX,并传入 Context
  • 然后,静态方法调用 Context 的 invoker 方法,而 invoker 方法内部会调用该 Context 所包含的Handler 的真正的 XXX 方法,调用结束后,如果还需要继续向后传递,就调用 Context 的 fireXXX2 方法,循环往复。

2.5 ChannelPipeline 调度 handler 梳理

  • Context 包装 handler,多个 Context 在 pipeline 中形成了双向链表,入站方向叫 inbound,由 head 节点开始。出站方法叫 outbound ,由 tail 节点开始
  • 而节点中间的传递通过 AbstractChannelHandlerContext 类内部的 fire 系列方法,找到当前节点的下一个节点不断的循环传播。是一个过滤器形式完成对 handler 的调度。

参考文档

Netty学习和源码分析github地址
Netty从入门到精通视频教程(B站)
Netty权威指南 第二版

猜你喜欢

转载自juejin.im/post/7101668180350730248