2019独角兽企业重金招聘Python工程师标准>>>
因项目需要,需要了解 Netty 这款号称 "高性能Java网络编程" 框架。拿起一本《Netty In Action》开始研究,在第2章的例子中,发现 Echo 服务端使用的ChannelHandler是 ChannelInboundHandlerAdapter ,而 Echo 客户端使用的却是 SimpleChannelInboundHandler 。一脸茫然,不知所措,只能点进去看各自的实现原理。
一.SimpleChannelInboundHandler
首先看到的是 SimpleChannelInboundHandler 继承自 ChannelInboundHandlerAdapter。
public abstract class SimpleChannelInboundHandler extends ChannelInboundHandlerAdapter{ ... }
有图有真相:
既然是继承关系,也就是说,"你有的我也有,你没有的我还有。" 那么 SimpleChannelInboundHandler 里面肯定重写或者新增了 ChannelInboundHandlerAdapter 里面的方法功能 - channelRead0 和 channelRead()。
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
这里只提供了一个模板,作用是把处理逻辑不变的内容写好在 channelRead(ctx,msg) 中,并且在里面调用 channelRead0 ,这样变化的内容通过抽象方法实现传递到子类中去了(在Netty5中channelRead0已被重命名为messageReceived)。
@Override// 继承了 ChannelInboundHandlerAdapter#channelRead
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);// 释放资源(保存消息的ByteBuf)
}
}
}
为什么这样做?看看《Netty In Action》的原话:
在客户端,当 channelRead0() 方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。
那为什么服务端不需要这样处理呢?
在EchoServerHandler中,你仍然需要将传入消息回送给发送者,而 write() 操作是异步的,直到 channelRead() 方法返回后可能仍然没有完成。为此,EchoServerHandler扩展了 ChannelInboundHandlerAdapter ,其在这个时间点上不会释放消息。
啥意思,我的理解是 ChannelInboundHandlerAdapter 不会像 SimpleChannelInboundHandler 一样在 channelRead() 里面释放资源,而是在收到消息处理完成的事件时,才会释放资源,看下面的代码就能理解了。
二.ChannelInboundHandlerAdapter
EchoServerHandler#channelReadComplete,这是一个EchoServer小例子:
public void channelReadComplete(ChannelHandlerContext ctx)
throws Exception {
//将未决消息冲刷到远程节点,并且关闭该 Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
消息在 channelReadComplete() 方法中,当 writeAndFlush() 方法被调用时才被释放,我们点进去源码验证一下:
AbstractChannelHandlerContext#writeAndFlush,如下所示,最后的资源是在 writeAndFlush() 中被释放的。
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());// 跳转到另外一个重载的方法中
}
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
if (msg == null) {// msg不能为空
throw new NullPointerException("msg");
}
if (isNotValidPromise(promise, true)) {
ReferenceCountUtil.release(msg);// 释放资源(保存消息的ByteBuf)
// cancelled
return promise;
}
write(msg, true, promise);// 异步写操作
return promise;
}
三.扩展 - ReferenceCountUtil
上面的源码中,最后资源是通过 ReferenceCountUtil 来释放的。也就是说,当我们需要释放ByteBuf相关内存的时候,也可以使用 ReferenceCountUtil#release()。
ReferenceCountUtil 底层实现是 ReferenceCounted ,当新的对象初始化的时候计数为1,retain() 方法被调用时引用计数加1,release()方法被调用时引用计数减1,当计数减少到0的时候会被显示清除,再次访问被清除的对象会出现访问冲突(这里想起了JVM判断对象是否存活的引用计数算法)。
ReferenceCountUtil#release:
public static boolean release(Object msg) {
if (msg instanceof ReferenceCounted) {
return ((ReferenceCounted) msg).release();// Decreases the reference count by 1
}
return false;
}