前言
虽说上一节讲了添加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:
虽说上面的是成功回调到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了。