ChannelPipeline:Netty的事件传播管道

235 阅读9分钟

Netty是一个异步的、高性能的、基于事件驱动的网络IO框架,它经过精心设计,不仅功能强大,还保持了良好的可扩展性,使用非常的灵活。

Netty的主要功能就是处理网络IO事件,它使用了「拦截过滤器」设计模式,ChannelHandler被设计为用来处理事件,而ChannelPipeline则用来传播事件。
ChannelPipeline.png
通过类图可以发现,ChannelPipeline实现了ChannelInboundInvoker和ChannelOutboundInvoker,这代表它可以传播入站/出站事件。实现了Iterable接口,是因为它管理着一组ChannelHandler,可以通过迭代器进行遍历。

ChannelPipeline被设计成一个双向链表,它由一系列ChannelHandlerContext组成,当ChannelHandler被添加到ChannelPipeline中时,会被包装成一个ChannelHandlerContext加入到链表中。这样,当有入站事件时,事件会通过ChannelPipeline传播,从链头的ChannelInBoundHandler一直被传递到链尾,每个ChannelHandler只处理自己感兴趣的事件,不感兴趣的事件则通过fileXXX()继续传播。

这样做的好处是,事件的处理会非常的灵活。每个ChannelHandler的职责可以非常的简单和清晰,例如只处理:握手认证、加解密、编解码等等,通过ChannelPipeline将这些ChannelHandler自定义组装,就可以构建出一个功能强大的Netty程序。
未命名文件.jpg
ChannelPipeline除了可以在硬编码时进行编排ChannelHandler,还支持运行时动态修改,这大大增加了它的灵活性。例如:针对IP白名单的请求,可以跳过握手认证。

1. DefaultChannelPipeline分析

ChannelPipeline有两大实现类:DefaultChannelPipeline和EmbeddedChannelPipeline,后者是测试ChannelHandler时才会用到,这里不会分析。

DefaultChannelPipeline是ChannelPipeline的默认实现类,很重要,这里会着重分析。

先看它的属性:

// ChannelHandler会绑定一个name,不指定的话会自动生成,这里会默认生成头尾节点的name
private static final String HEAD_NAME = generateName0(HeadContext.class);
private static final String TAIL_NAME = generateName0(TailContext.class);

// 缓存ChannelHandler对应的name,避免重复生成
private static final FastThreadLocal<Map<Class<?>, String>> nameCaches =
    new FastThreadLocal<Map<Class<?>, String>>() {
    @Override
    protected Map<Class<?>, String> initialValue() {
        return new WeakHashMap<Class<?>, String>();
    }
};

// 便于CAS修改estimatorHandle
private static final AtomicReferenceFieldUpdater<DefaultChannelPipeline, MessageSizeEstimator.Handle> ESTIMATOR =
    AtomicReferenceFieldUpdater.newUpdater(
    DefaultChannelPipeline.class, MessageSizeEstimator.Handle.class, "estimatorHandle");

// 默认的头、尾节点,分别是HeadContext,TailContext
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;

// Pipeline绑定的Channel
private final Channel channel;
private final ChannelFuture succeededFuture;
private final VoidChannelPromise voidPromise;
// 是否开启内存泄漏追踪?如果开启了,调用touch()会记录堆栈信息
private final boolean touch = ResourceLeakDetector.isEnabled();
// 缓存Group对应的Group.next()事件执行器
private Map<EventExecutorGroup, EventExecutor> childExecutors;
// 计算ByteBuf占用的内存
private volatile MessageSizeEstimator.Handle estimatorHandle;
// 是否首次注册,如果是,会触发HandlerAdded回调
private boolean firstRegistration = true;

// ChannelHandler从Pipeline添加/移除后会触发回调,这里会将回调列表通过单向链表连接起来
private PendingHandlerCallback pendingHandlerCallbackHead;

// Pipeline是否注册到Channel
private boolean registered;

比较重要的有head和tail,分别记录这ChannelPipeline的头尾节点,DefaultChannelPipeline头尾节点是固定的,分别是HeadContext和TailContext,它俩有特殊的用途,不可修改。

ChannelHandler用途
HeadContext1.传播入站事件
2.需要和JDK底层API交互的,转交给Unsafe执行
TailContext1.释放入站数据
2.报告异常

再看它的构造函数,会给头尾节点赋值,并形成双向链表。

protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    /*
    处理入站事件:
        1.如果前面的所有Handler都没释放入站数据,则TailContext会负责释放,防止内存泄漏。
        2.前面的Handler都没处理异常,则TailContext会打印警告日志,释放资源。
     */
    tail = new TailContext(this);

    /*
    处理出/入站事件:
        1.入站事件,无脑向后传播。
        2.出站事件,转交给Channel.Unsafe执行
     */
    head = new HeadContext(this);

    // 形成双向链表,中间可能还会插入ChannelHandlerContext
    head.next = tail;
    tail.prev = head;
}

1.1 编排ChannelHandler

ChannelPipeline的主要作用就是ChannelHandler的编排容器,通过将不同功能的ChannelHandler组合起来,就可以快速构建功能各异的Netty程序。

添加ChannelHandler的方法如下:

方法说明
addFirst()将ChannelHandler添加到HeadContext的下一个
addLast()将ChannelHandler添加到TailContext的上一个
addBefore()添加到指定Handler之前
addAfter()添加到指定Handler之后

移除ChannelHandler的方法如下:

方法说明
remove()将指定ChannelHandler从链表中移除
removeFirst()移除HeadContext的下一个节点
removeLast()移除TailContext的上一个节点

由于使用的是链表的数据结构,因此ChannelHandler的增删效率极高,只是简单的改变指针的指向即可。

通过调用以上方法,就可以编排ChannelHandler的顺序,以处理IO事件。

代码太多,不全部贴了,这里只分析addFirst()的源码,其他大家自行研究。

@Override
public final ChannelPipeline addFirst(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    // 支持运行时并发修改,因此必须做同步控制
    synchronized (this) {
        // 检查ChannelHandler是否允许被添加,非共享的ChannelHandler只能被添加一次
        checkMultiplicity(handler);
        // 校验ChannelHandler的name是否重复,没有给定name会自动生成,按Handler类型+序号
        name = filterName(name, handler);
        // 将Handler封装为ChannelHandlerContext
        newCtx = newContext(group, name, handler);

        // 添加到HeadContext的next
        addFirst0(newCtx);

        // 如果非首次注册,则添加Handler回调任务
        if (!registered) {
            newCtx.setAddPending();
            callHandlerCallbackLater(newCtx, true);
            return this;
        }

        // 首次注册,如果是EventLoop线程,则直接触发Handler的handlerAdded()回调
        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }
    // 触发Handler的handlerAdded()回调
    callHandlerAdded0(newCtx);
    return this;
}

addFirst0()会将ChannelHandlerContext添加到HeadContext的next上:

// 将ChannelHandlerContext添加到HeadContext的next
private void addFirst0(AbstractChannelHandlerContext newCtx) {
    // 修改三者的指针指向
    AbstractChannelHandlerContext nextCtx = head.next;
    newCtx.prev = head;
    newCtx.next = nextCtx;
    head.next = newCtx;
    nextCtx.prev = newCtx;
}

addLast()也是同理,并非添加到链尾,而是添加到TailContext的prev上。

1.2 fileXXX()传播事件

ChannelPipeline的fileXXX()会将事件传播给后续的ChannelHandler执行,事件传播方法如下:

方法说明
fireChannelRegistered()传播Channel注册事件
fireChannelUnregistered()传播Channel取消注册事件
fireChannelActive()传播Channel活跃事件
fireChannelInactive()传播Channel失活事件
fireExceptionCaught()传播异常事件
fireUserEventTriggered()传播用户自定义事件
fireChannelRead()传播Channel可读事件
fireChannelReadComplete()传播Channel读取完毕事件
fireChannelWritabilityChanged()传播Channel可写状态变更事件

由于大量的代码是重复的,笔者就不贴所有代码了,以fireChannelRead()为例进行分析。

当NioEventLoop检测到Selector上的Channel有数据可读时,会执行Channel.Unsafe.read()方法读取数据并封装成ByteBuf,通过Pipeline将事件传播出去,这样后续的ChannelHandler就可以拿到ByteBuf做自己的业务了。

ChannelPipeline.fireChannelRead()会将读取到的数据从HeadContext开始传播。

// 传播可读事件,将读取到的msg 进行传递
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
    // 从链头开始传播,即从HeadContext开始
    AbstractChannelHandlerContext.invokeChannelRead(head, msg);
    return this;
}

invokeChannelRead()会执行给定ChannelHandler的ChannelRead回调。

/**
 * 执行指定ChannelHandler的ChannelRead回调
 * @param next 给定的ChannelHandlerContext节点
 * @param msg 读取到的数据
 */
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
    // 调用touch()的目的只是为了追踪对象的访问堆栈记录,用于内存泄漏的排查
    final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
    // 获取Context绑定的EventLoop
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        // 如果是EventLoop线程,则立即执行
        next.invokeChannelRead(m);
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRead(m);
            }
        });
    }
}

invokeChannelRead()会检查ChannelHandler的handlerAdded回调是否触发,如果没触发,会跳过该Handler,将事件继续向后传播,否则就直接执行channelRead回调。

private void invokeChannelRead(Object msg) {
    // 检查handlerAdded回调是否触发,如果没触发则暂时跳过该Handler。
    if (invokeHandler()) {
        try {
            // handlerAdded回调已触发,执行channelRead回调
            ((ChannelInboundHandler) handler()).channelRead(this, msg);
        } catch (Throwable t) {
            invokeExceptionCaught(t);
        }
    } else {
        // handlerAdded回调还没触发,继续向后传播
        fireChannelRead(msg);
    }
}

1.3 HeadContext

DefaultChannelPipeline除了传播事件,它内置的头尾节点也是非常重要,有它们本身的职责所在。
HeadContext.png
HeadContext本身是一个ChannelHandlerContext,它会被添加到ChannelPipeline的链头,同时它实现了ChannelInboundHandler和ChannelOutboundHandler接口,这代表它需要处理入站和出站事件。

对于入站事件,HeadContext必须向后传播,否则后续的ChannelHandler就没法处理了,下面是channelRead的一个例子:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    // 入站事件直接向后传播
    ctx.fireChannelRead(msg);
}

@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
    // 向后找到能处理CHANNEL_READ事件的InBoundHandler,再执行它的ChannelRead事件
    invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
    return this;
}

对于出站事件,如bind、write等需要调用JDK底层API的操作,HeadContext会转交给Unsafe去执行。这里拿write为例:

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    // 需要和JDK底层API交互,转交给Unsafe执行。
    unsafe.write(msg, promise);
}

Unsafe会将ByteBuf暂存到ChannelOutboundBuffer,flush会将flushde节点转换成Java的ByteBuffer,再调用底层APISocketChannel.write(ByteBuffer)将数据发送出去。

HeadContext这里做了一个代理的角色,实际出站事件的执行会转交给Unsafe。

1.4 TailContext

TailContext.png
HeadContext本身是一个ChannelHandlerContext,它会被添加到ChannelPipeline的链尾,同时它实现了ChannelInboundHandler接口,这代表它需要处理入站事件。

由于是被编排在ChannelPipeline的链尾,这意味着入站事件它总是最后处理的,这使得它可以做一些保护性的动作,避免程序出现问题。

例如:如果前面所有的ChannelHandler都没有释放入站数据,TailContext会负责释放,避免内存泄漏。源码如下:

protected void onUnhandledInboundMessage(Object msg) {
    try {
        logger.debug(
            "Discarded inbound message {} that reached at the tail of the pipeline. " +
            "Please check your pipeline configuration.", msg);
    } finally {
        // 释放入站数据
        ReferenceCountUtil.release(msg);
    }
}

如果前面所有的ChannelHandler都没有处理异常事件,TailContext会输出日志,记录下未被处理的异常。源码如下:

protected void onUnhandledInboundException(Throwable cause) {
    try {
        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);
    } finally {
        ReferenceCountUtil.release(cause);
    }
}

TailContext主要的主责就是释放数据和记录异常了,其他事件它并不感兴趣,大量方法是空的,这里就不贴代码了。

2. 总结

ChannelPipeline是ChannelHandler的编排容器,DefaultChannelPipeline是Netty提供的默认实现类,它本身是一个双向链表,由一系列ChannelHandlerContext组成,ChannelHandler被添加到ChannelPipeline时,会被封装成ChannelHandlerContext并加入到链表。

ChannelPipeline的事件传播机制,使得Netty在处理IO事件时非常的灵活,每个ChannelHandler的职责都非常清晰,实现解耦,通过组装各个ChannelHandler就可以快速构建一个功能各异的Netty程序。

HeadContext和TailContext是DefaultChannelPipeline默认的头尾节点,它们的职责也非常重要。HeadContext需要将入站事件向后传播,否则后续的ChannelHandler就没法处理事件了,对于出站事件需要和JDK底层API交互的,需要转交给Unsafe类执行。TailContext会负责入站数据的释放和记录异常,做一些保护性的工作。