ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 的区别

优质博文:IT-BLOG-CN

如下就是两个类的声明,SimpleChannelInboundHandler是继承 ChannelInboundHandlerAdapter的。也就是说SimpleChannelInboundHandler 也拥有 ChannelInboundHandlerAdapter的方法。

java 复制代码
//ChannelInboundHandlerAdapter 类
ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler
//SimpleChannelInboundHandler 类
SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter

继承关系图:

SimpleChannelInboundHandlerchannelRead相比 ChannelInboundHandlerAdapter而言,主要做了类型匹配以及用完之后释放指向保存该消息的 ByteBuf的内存引用。这里提供了一个模板,作用是把处理逻辑不变的内容写好在channelRead(ctx,msg) 中,并且在里面调用 channelRead0 ,这样变化的内容通过抽象方法实现传递到子类中去了(在Netty5channelRead0已被重命名为 messageReceived)。

java 复制代码
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    boolean release = true;

    try {
        if (this.acceptInboundMessage(msg)) {
            this.channelRead0(ctx, msg);
        } else {
            release = false;
            ctx.fireChannelRead(msg);
        }
    } finally {
        if (this.autoRelease && release) {
            ReferenceCountUtil.release(msg);
        }
    }
}

protected abstract void channelRead0(ChannelHandlerContext var1, I var2) throws Exception;

相比之下,ChannelInboundHandlerAdapter好像一无是处,毕竟他要自己处理资源的释放,例如如下的调用:buf.release();如果说 channelRead都是同步操作的话,SimpleChannelInboundHandler是不错的选择,如果操作是异步的话,那他的逻辑就有点麻烦了,例如你把数据交给另外的线程处理了,还没处理就会释放了 。这里必须说明一个问题,他的回收和 jvm的垃圾回收还不完全是一回事。netty是自己做了引用计数的操作。 buf.refCnt(); 通过上面的 api就可以获取到计数的个数。具体的引用计数的部分,不知道也不影响 netty的学习,这个点后面具体再说。ChannelInboundHandlerAdapter 处理自由的优点也就提现出来了,可以更好的处理更多的特定场景。

那该方法是什么时候释放资源的呢?writeAndFlush() 方法被调用时才被释放,我们点进去源码验证一下:

java 复制代码
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来释放的。也就是说,当我们需要释放 ByteBuf相关内存的时候,也可以使用 ReferenceCountUtil.release()ReferenceCountUtil 底层实现是 ReferenceCounted ,当新的对象初始化的时候计数为1,retain() 方法被调用时引用计数加1,release()方法被调用时引用计数减1,当计数减少到0的时候会被显示清除,再次访问被清除的对象会出现访问冲突(这里想起了JVM判断对象是否存活的引用计数算法)。ReferenceCountUtil.release源码如下:

java 复制代码
public static boolean release(Object msg) {
    if (msg instanceof ReferenceCounted) {
        return ((ReferenceCounted) msg).release();// Decreases the reference count by 1
    }
    return false;
}

SimpleChannelInboundHandlerChannelInboundHandlerAdapter区别:

在客户端的业务 Handler继承的是 SimpleChannelInboundHandler,而在服务器端继承的是 ChannelInboundHandlerAdapter。最主要的区别就是 SimpleChannelInboundHandler在接收到数据后会自动 release掉数据占用的 Bytebuffer资源(自动调用Bytebuffer.release())。而为何服务器端不能用呢,因为我们想让服务器把客户端请求的数据发送回去,而 write() 操作是异步的,而服务器端有可能在channelRead方法返回后还没有写完数据,因此不能让它自动releaseSimpleChannelInboundHandler 是有泛型参数的。可以指定一个具体的类型参数,通过 decoder配合使用,非常方便。ChannelInboundHandlerAdapter 则是直接操作 byte数组的。

SimpleChannelInboundHandler 的好处是可以处理不同的类型对象,并且可以做释放。ChannelInboundHandlerAdapter的好处则是更自由,在异步的场景下更适合。

ChannelInboundHandlerAdapter 注意事项 : 用户自定义了一个 Handler类,代码如下:

java 复制代码
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    this.ctx = ctx;
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    super.channelRead(ctx, msg);
    log.debug("thread.name={}", Thread.currentThread().getName());

    ByteBuf in = (ByteBuf) msg;

    String readStr = in.toString(CharsetUtil.UTF_8);
    log.debug("Server received: {}", readStr);

    log.debug("release msg");
    ReferenceCountUtil.release(msg);
 }

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    ctx.flush();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.close();
}

行时出现了如下堆栈异常:

java 复制代码
io.netty.util.IllegalReferenceCountException: refCnt: 0
at io.netty.buffer.AbstractByteBuf.ensureAccessible(AbstractByteBuf.java:1173)
at io.netty.buffer.AbstractByteBuf.checkIndex(AbstractByteBuf.java:1119)
at io.netty.buffer.UnpooledUnsafeDirectByteBuf.internalNioBuffer(UnpooledUnsafeDirectByteBuf.java:385)
at io.netty.buffer.ByteBufUtil.decodeString(ByteBufUtil.java:568)
at io.netty.buffer.AbstractByteBuf.toString(AbstractByteBuf.java:979)
at io.netty.buffer.AbstractByteBuf.toString(AbstractByteBuf.java:974)
at com.spy.apollo.netty.demo.demo02_biz_logic.ServerBizHandler.channelRead(ServerBizHandler.java:50)

异常说是引用计数 refCnf = 0,也就是没有被引用,因此报错;常规的 channelRead中消息读取完毕是要立即释放当前消息的引用计数即(减一操作)如下代码操作。但是其他 Handler也去释放时,发现没有引用,就会报错。

java 复制代码
ReferenceCountUtil.release(msg);

分析: 通过调试代码发现根源就在 super.channelRead(ctx, msg);这个函数。其实 ChannelInboundHandlerAdapter 是提供了一种默认实现,子类如果要继承,需要覆盖父类中的方法,并且不需要调用 super.xxxxMethod()。源码如下:意思是此实现只是将操作转发到下一个 channelhandler。子类可以重写方法实现来改变这一点。

java 复制代码
/**
 * <p>
 * This implementation just forward the operation to the next {@link ChannelHandler} in the
 * {@link ChannelPipeline}. Sub-classes may override a method implementation to change this.
 * </p>
 */
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
相关推荐
名字还没想好☜2 小时前
Python f-string 进阶:数字格式化、对齐填充、调试 = 号与嵌套表达式
开发语言·数据库·python·字符串格式化·f-string
LucianaiB2 小时前
毕业老学长给我留的最后一句话是:“AI 写的,别挂我名。” 我直接 神(Seed Evolving) 来!助我!
后端
IvanCodes2 小时前
RAG 实战教程(一):RAG 工作原理与完整流程——分片、索引、召回、重排和生成
人工智能·后端·agent
·薯条大王2 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
CodeSheep3 小时前
稚晖君公司人事大变动,来了!
前端·后端·程序员
小满zs3 小时前
Go语言第八章(函数)
后端·go
kyriewen5 小时前
面试官问"这段逻辑为什么这样写"——我才发现,这半年我写的代码,我自己都解释不了
前端·javascript·面试
stark张宇5 小时前
实战Go高级特性:Context超时控制、defer资源回收与Channel通信的关键避坑点
后端·go
一壶浊酒..5 小时前
Scrape Webpack练习
开发语言·javascript·webpack
ZJH__GO5 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络