前言

虽说上一节讲了添加handler的流程,但是第一节推导出的理论仍然非常重要,如果对前两节的内容脑海里没有一个印象,建议先回去复习一下。

而本文就来跟进一下删除ChannelHandler的代码逻辑。

Netty Version:4.1.6


应用场景

删除ChannelHandler的应用场景显然没有添加ChannelHandler的应用场景多,但是在某些流程中,仍然发挥着很重要的作用,下面就来举两个例子:

  • 首次连接时需要一个handler推送消息,推送完之后删除handler。
  • 新连接接入时验证发送的密钥,验证完后删除handler。

大致流程

删除节点的大致流程如下:

  • 先根据handler找到要删除的节点,如果存在就返回该节点。
  • 将节点从双向链表中删除。
  • 回调handlerRemoved事件。

实验代码

Server.java

import com.imooc.netty.ch3.ServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.AttributeKey;

public final class Server {

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(1);

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new AuthHandler());
                        }
                    });

            ChannelFuture f = b.bind(8888).sync();

            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

AuthHandler.java

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class AuthHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        if (!paas(msg)) {
            ctx.pipeline().remove(this);
        } else {
            // do something
        }
    }

    /**
     * 模拟验证不通过
     */
    private boolean paas(ByteBuf password) {
        return false;
    }
}
  • 可能你会疑问为什么突然冒出个SimpleChannelInboundHandler,前面好像没讲过啊?无需在意,channelRead0只是channelRead的其中一环,之所以覆写channelRead0,是因为channelRead在执行完channelRead0后无需手动传播事件到tail节点进行回收,而是自动回收。

关于事件是如何传到tail的,会在下一篇博客写。


跟进源码

寻找节点

至于打断点、telnet连接这些繁琐因人而异的步骤我就不再一一赘述了,下面准备开始跟源码。

先将视角拉到AuthHandler.java的remove方法中,启动Server.java代码,然后telnet连接并随便发送些数据,断点就能来到这个remove方法了,我们跟下去:

此处【坐标1】
io.netty.channel.DefaultChannelPipeline#remove(io.netty.channel.ChannelHandler)

    @Override
    public final ChannelPipeline remove(ChannelHandler handler) {
        remove(getContextOrDie(handler));
        return this;
    }

getContextOrDie就是根据handler获取节点的方法,跟进去:
io.netty.channel.DefaultChannelPipeline#getContextOrDie(io.netty.channel.ChannelHandler)

    private AbstractChannelHandlerContext getContextOrDie(ChannelHandler handler) {
        AbstractChannelHandlerContext ctx = (AbstractChannelHandlerContext) context(handler);
        if (ctx == null) {
            throw new NoSuchElementException(handler.getClass().getName());
        } else {
            return ctx;
        }
    }
  • 如果找不到节点则抛出异常。

跟进context方法:
io.netty.channel.DefaultChannelPipeline#context(io.netty.channel.ChannelHandler)

    @Override
    public final ChannelHandlerContext context(ChannelHandler handler) {
        if (handler == null) {
            throw new NullPointerException("handler");
        }

        AbstractChannelHandlerContext ctx = head.next;
        for (;;) {

            if (ctx == null) {
                return null;
            }

            if (ctx.handler() == handler) {
                return ctx;
            }

            ctx = ctx.next;
        }
    }
  • 可见其实就是遍历pipeline中存储的双向链表,比对它们的handler,如果是目标handler,则返回当前节点。

删除节点

现在将视角转回【坐标1】的remove方法,跟进去:
此处【坐标2】
io.netty.channel.DefaultChannelPipeline#remove(io.netty.channel.AbstractChannelHandlerContext)

    private AbstractChannelHandlerContext remove(final AbstractChannelHandlerContext ctx) {
        assert ctx != head && ctx != tail;

        synchronized (this) {
            remove0(ctx);

            if (!registered) {
                callHandlerCallbackLater(ctx, false);
                return ctx;
            }

            EventExecutor executor = ctx.executor();
            if (!executor.inEventLoop()) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerRemoved0(ctx);
                    }
                });
                return ctx;
            }
        }
        callHandlerRemoved0(ctx);
        return ctx;
    }
  • 断言那一行告诉我们是不允许删除head和tail节点
  • remove0方法就是删除节点。
  • callHandlerRemoved0方法就是回调handlerRemoved事件。

这里先跟进remove0方法:
io.netty.channel.DefaultChannelPipeline#remove0

    private static void remove0(AbstractChannelHandlerContext ctx) {
        AbstractChannelHandlerContext prev = ctx.prev;
        AbstractChannelHandlerContext next = ctx.next;
        prev.next = next;
        next.prev = prev;
    }
  • 从双向链表中删除节点(注意节点本身并没有删除)。

回调handlerRemoved事件

视角拉回到【坐标2】的代码,跟进callHandlerRemoved0方法:
io.netty.channel.DefaultChannelPipeline#callHandlerRemoved0

    private void callHandlerRemoved0(final AbstractChannelHandlerContext ctx) {
        // Notify the complete removal.
        try {
            try {
                ctx.handler().handlerRemoved(ctx);
            } finally {
                ctx.setRemoved();
            }
        } catch (Throwable t) {
            fireExceptionCaught(new ChannelPipelineException(
                    ctx.handler().getClass().getName() + ".handlerRemoved() has thrown an exception.", t));
        }
    }
  • ctx.handler返回的就是AuthHandler:image.png

虽说上面的是成功回调到handlerRemoved事件了,但由于我们的AuthHandler并没有覆写handlerRemoved方法,所以最后是回调到了ChannelHandler的handlerRemoved方法,这个方法什么都没做:
io.netty.channel.ChannelHandlerAdapter#handlerRemoved

    /**
     * Do nothing by default, sub-classes may override this method.
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // NOOP
    }


SimpleChannelInboundHandler自动回收

在执行完前面的代码逻辑ctx.pipeline().remove(this)后,由于目标节点仅仅是从双向链表中删除,但是节点对象还并未消失,也就是说有OOM的风险,那是不是就意味着我们还要在channelRead0方法中执行完remove后,还需要手动释放节点 或者 手动把节点传播到tail回收呢?

其实Netty为用户考虑到这一点了,给我们提前写好一些自动处理的逻辑,其中一个就是AuthHandler继承的SimpleChannelInboundHandler。

AuthHandler覆写了一个channelRead0方法,根据前两节的知识,我们都知道,真正被回调的方法应该叫channelRead,所以现在就来看看SimpleChannelInboundHandler的channelRead方法:
io.netty.channel.SimpleChannelInboundHandler#channelRead

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I imsg = (I) msg;
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }
  • 可见channelRead0方法就是channelRead方法内的其中一步。
  • channelRead0就是提供给用户编写自己的逻辑的,等channelRead0执行完后,用户不需要担心对象回收,因为channelRead会继续执行下去,回收当前节点。


小结

  • 删除ChannelHandler的源码逻辑还是十分简单的,就是根据handler匹配节点,然后再将节点从双向链表删除。
  • 上一节的添加ChannelHandler,在最后都会回调某个事件。
  • 为了用户使用更便利,Netty给我们提供了一些实现了含有自动逻辑处理的父类,开发常用的可能就是SimpleChannelInboundHandler了。