第6章 ChannelHandler和ChannelPipeline

第6章 ChannelHandler和ChannelPipeline

6.1 ChannelHandler家族

6.1.1 Channel的生命周期

    1. ChannelUnregistered ----- Channel已经被创建,但未注册到EventLoop上
    1. ChannelRegistered ----- Channel已经被注册到EventLoop上
    1. ChannelActive ----- Channel处于活动状态。对于Tcp客户端是只有与远程建立连接后,channel才会变成Active。udp是无连接的协议,Channel一旦被打开,便激活。注意这点不同点
    1. ChannelInactive ----- Channel处于关闭状态。常用来发起重连或切换链路

6.1.2 ChannelHandler的生命周期

ChannelHandler被添加到ChannelPipeline中或者被从ChannelPipeline中移除时将调用下列操作:

    1. handlerAdded ----- ChannelHandler被添加到ChannelPipeline中时被触发
    1. handlerRemoved ----- ChannelHandler被从ChannelPipeline中移除时触发
    1. exceptionCaught ----- 处理过程中发生异常,则触发

Netty提供了两个重要的ChannelHandler子接口:

    1. ChannelInboundHandler ----- 处理入站数据和入站事件
    1. ChannelOutboundHandler ----- 处理出站数据并且允许拦截所有的操作

6.1.3 ChannelInboundHandler接口

ChannelInboundHandler接口处理入站事件和入站数据,提供的事件方法如下图:

表6-3

提醒:

解释上图中的几个方法,帮助理解与学习:

    1. channelReadComplete ----- Channel一次读操作完成时被触发,开始准备切换为写操作。Channel是一个数据载体,既可以写入数据,又可以读取数据。所以存在读操作和写操作切换。
    1. channelWritabilityChanged ----- 帮助用户控制写操作速度,以避免发生OOM异常。通过Channel.config().setWriteHighWaterMark()设置发送数据的高水位。
    1. userEventTriggered ----- 用户事件触发。Netty提供心跳机制中使用,请参考netty-private-protocol开发子项目,参考
    1. userEventTriggered ----- 实现用户自定义事件,完成ChannelPipeline动态编排效果的实现。请参考另一个子项目中动态编排ChannelHandler案例,参考

当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它将负责显式地
释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法 ReferenceCountUtil.release(),如下

 // @Sharable标注一个channel handler可以被多个channel安全地共享。
 @Sharable   
    public class DiscardHandler extends ChannelInboundHandlerAdapter {

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            //丢弃已接收的消息
            ReferenceCountUtil.release(msg);
        }
    } 

Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现
违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用 SimpleChannelInboundHandler。

@Sharable
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, Object msg) {
            //不需要任何显式的资源释放
            // No need to do anything special
        } 
    }

注:由于SimpleChannelInboundHandler中的channelRead已经使用了ReferenceCountUtil.release(msg);
源码如下

 @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);
            }
        }
    }

由于 SimpleChannelInboundHandler 会自动释放资源,所以你不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。

6.1.4 ChannelOutboundHandler接口

出站操作和数据将由 ChannelOutboundHandler 处理。它的方法将被 Channel、ChannelPipeline 以及 ChannelHandlerContext 调用。

ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,
这使得可以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲 刷操作并在稍后继续。

ChannelOutboundHandler大部分方法都需要一个ChannelPromise参数,以便在操作完成时得到通知。

    1. ChannelPromise是ChannelFuture的一个子类,使用setSuccess()和setFailure()方法告知操作结果。ChannelPromise设置结果后,将变成不可修改对象。

6.1.5 ChannelHandler适配器

Netty提供两个ChannelHandler适配器: ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。通常自己实现处理业务的Handler都是继承这两个适配器

这两个适配器分别提供了 ChannelInboundHandler和 ChannelOutboundHandler 的基本实现。
通过扩展抽象类 ChannelHandlerAdapter,它们获得了它们共同的超接口 ChannelHandler 的方法。

    1. ChannelHandlerAdapter适配器中的一个使用的方法: isSharable() ----- 如果其对应的实现被标注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline中

在 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 中所提供的方法体调用了其相关联的ChannelHandlerContext上的等效方法,从而将事件转发到了 ChannelPipeline 中的下一个 ChannelHandler 中。

6.1.6 资源管理

Netty使用的ByteBuf采用的是引用计数机制来回收。对于初学者非常容易造成资源泄漏。Netty提供以下帮助定位资源泄漏代码。推荐使用Java系统属性设置方法: java -Dio.netty.leadDetectionLevel=ADVANCED

每当通过调用 ChannelInboundHandler.channelRead()或者 ChannelOutboundHandler.write()方法来处理数据时,你都需要确保没有任何的资源泄漏。Netty 使用引用计数来处理池化的 ByteBuf。所以在完全使用完某个ByteBuf 后,调整其引用计数是很重要的。

表6-5

为了帮助你诊断潜在的(资源泄漏)问题,Netty提供了class ResourceLeakDetector , 它将对你应用程序的缓冲区分配做大约 1%的采样来检测内存泄露。相关的开销是非常小的。如果检测到了内存泄露,将会产生类似于下面的日志消息:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable
advanced leak reporting to find out where the leak occurred. To enable
advanced leak reporting, specify the JVM option
'-Dio.netty.leakDetectionLevel=ADVANCED' or call
ResourceLeakDetector.setLevel().
如何管理好资源:

想要管理好资源,避免资源浪费,请记住以下几点:

    1. 三种ByteBuf(堆缓冲区、直接缓冲区和复合缓冲区)都采用的引用计数方式维护对象。所以都可能需要程序员参与管理资源。对于刚使用ByteBuf的程序员来说,存在误区:以为只有直接缓冲区才使用引用计数。
    1. 如果当前ByteBuf被Channel调用write(...)或writeAndFlush(...)方法,则Netty会自动执行引用计数减1操作,释放该ByteBuf
    1. 谁负责释放: 一般来说,是由最后访问(引用计数)对象的来负责释放该对象
    1. 如果是SimpleChannelInboundHandler的子类,传入的参数msg,会被SimpleChannelInboundHandler自动释放一次

消费并释放入站消息

@Sharable
    public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            //通过调用 ReferenceCountUtil.release()方法释放资源
            ReferenceCountUtil.release(msg);
        } 
    }

消费入站消息的简单方式:
由于消费入站数据是一项常规任务,所以 Netty 提供了一个特殊的被称为 SimpleChannelInboundHandler 的 ChannelInboundHandler 实现。这个实现会在消息被 channelRead0()方法消费之后自动释放消息。

@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);
            }
        }
    }

丢弃并释放出站消息

@Sharable
    public class DiscardOutboundHandler
            extends ChannelOutboundHandlerAdapter {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            //通过使用 ReferenceCountUtil.realse(...)方法释放资源
            ReferenceCountUtil.release(msg);
            //通知 ChannelPromise数据已经被处理了
            promise.setSuccess();
        } 
    }

重要的是,不仅要释放资源,还要通知 ChannelPromise。否则可能会出现 ChannelFutureListener 收不到某个消息已经被处理了的通知的情况。
总之,如果一个消息被消费或者丢弃了,并且没有传递给 ChannelPipeline 中的下一个ChannelOutboundHandler,那么用户就有责任调用 ReferenceCountUtil.release()。
如果消息到达了实际的传输层,那么当它被写入时或者 Channel 关闭时,都将被自动释放。

6.2 ChannelPipeline接口

ChannelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler实例链。需要记住以下几个重要的点:

图6-3

    1. ChannelHandler是组成ChannelPipeline链的节点,也就是对应于上图的入站处理器和出站处理器
    1. ChannelPipeline的头部和尾部是固定不变的。如上图6.3所示
    1. 在一个ChannelPipeline链上,ChannelHandlerContext与ChannelHandler是1:1对应的。也就是说,每一个ChannelHandler都有一个自己的ChannelHandlerContxt。后文会详细讲述
    1. 每次Channel收到的消息,流转路径是: 头部 -> 尾部。每次Channel调用一次write操作时,流转路径是: 尾部 -> 头部
    1. 重要的事情说三遍: 不要阻塞ChannelChandler,不要阻塞ChannelChandler,不要阻塞ChannelChandler。否则,可能会影响其他的Channel处理。原因见:3.1.2章节

ChannelHandlerContext 使得ChannelHandler能够和它的ChannelPipeline以及其他的ChannelHandler 交互。 ChannelHandler 可以通知其所属的 ChannelPipeline 中的下一 个ChannelHandler,甚至可以动态修改它所属的ChannelPipeline

6.2.1 修改ChannelPipeline

ChannelHandler 可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改
ChannelPipeline 的布局。(它也可以将它自己从 ChannelPipeline 中移除。)这是 ChannelHandler 最重要的能力之一

ChannelPipeline pipeline = ..;
FirstHandler firstHandler = new FirstHandler();

//将该实例作为"handler1"添加到ChannelPipeline 中
pipeline.addLast("handler1", firstHandler);

//将一个 SecondHandler的实例作为"handler2"添加到 ChannelPipeline的第一个槽中。
//这意味着它将被放置在已有的"handler1"之前
pipeline.addFirst("handler2", new SecondHandler());

pipeline.addLast("handler3", new ThirdHandler());

...
//通过名称移除"handler3"
pipeline.remove("handler3");

//通过引用移除FirstHandler(它是唯一的,所以不需要它的名称)
pipeline.remove(firstHandler);
//将 SecondHandler("handler2")替换为 FourthHandler:"handler4"
pipeline.replace("handler2", "handler4", new ForthHandler());

ChannelHandler 的执行和阻塞
通常 ChannelPipeline 中的每一个 ChannelHandler 都是通过它的 EventLoop(I/O 线程)来处
理传递给它的事件的。所以至关重要的是不要阻塞这个线程,因为这会对整体的 I/O 处理产生负面的影响。
但有时可能需要与那些使用阻塞 API 的遗留代码进行交互。对于这种情况,ChannelPipeline 有一些
接受一个 EventExecutorGroup 的 add()方法。如果一个事件被传递给一个自定义的 EventExecutorGroup,它将被包含在这个 EventExecutorGroup 中的某个 EventExecutor 所处理,从而被从该Channel 本身的 EventLoop 中移除。对于这种用例,Netty 提供了一个叫 DefaultEventExecutorGroup 的默认实现。

表6-7

6.2.2 入站操作和出站操作

ChannelPipeline入站操作
表6-8

ChannelPipeline出站操作
表6-9

  • ChannelPipeline 保存了与 Channel 相关联的 ChannelHandler; 
  • ChannelPipeline 可以根据需要,通过添加或者删除 ChannelHandler 来动态地修改; 
  • ChannelPipeline 有着丰富的 API 用以被调用,以响应入站和出站事件。

6.3 ChannelHandlerContext 接口

ChannelHandlerContext源码

    1. ChannelHandlerContext对象实例与ChannelHandler对象实例的关系是n:1的关系。如果从单个ChannelPipeline来看,一个ChannelHandlerContext对象实例对应于一个ChannelHandler对象实例.
    1. ChannelHandlerContext的许多方法与Channel或者ChannelPipeline上方法类似。但是有一点非常大的不同点: 调用Channel或者ChannelPipeline上的这些方法,将沿着整个ChannelPipeline进行传播。而调用ChannelHandlerContext上的相同方法,则将从当前关联的Channelhandler开始,并且只会传播给位于该ChannelPipeline上的下一个能够处理该事件的ChannelHandler。
    1. handler() ----- 返回绑定到这个实例的ChannelHandler。
      表6-10
注意点:
    1. ChannelHandlerContext和ChannelHandler之间的关联是永远不变的,所以缓存对它的引用是安全且可行的。如上6.3章节中第一点所描述的。
    1. 相对于Channel和ChannelPipeline上的方法,ChannelHandlerContext的方法将产生更短的事件流(解释如上述6.3章节的第二点),所以性能也会更优秀。

6.3.1 使用ChannelHandlerContext

下图充分说明了ChannelHandlerContext在ChannelPipeline充当的作用,我们可以从图中发现

    1. 对于单个ChannelPipeline来看,ChannelHandlerContext和ChannelHandler的关联关系是1:1
    1. ChannlePipeline中事件的传递,原来是依赖于ChannelHandlerContext实现的。
    1. 图中AContext将事件(read)传递给BHandler,BContext再将事件(read)传递给了Chandler。
    1. 如果想从特定的Handler传播事件,需要获取上一个ChannelHandlerContext。比如:希望事件从CHandler开始传播,跳过AHandler和BHandler,则获取到BContext即可。

图6-4

将通过 ChannelHandlerContext 获取到 Channel 的引用。调用Channel 上的 write()方法将会导致写入事件从尾端到头部地流经 ChannelPipeline。

ChannelHandlerContext ctx = ..;
//获取到与 ChannelHandlerContext相关联的 Channel 的引用
Channel channel = ctx.channel();
//通过 Channel 写入缓冲区
channel.write(Unpooled.copiedBuffer("Netty in Action",CharsetUtil.UTF_8));

下面是写入 ChannelPipeline。(到 ChannelPipline 的)引用是通过 ChannelHandlerContext 获取的。

ChannelHandlerContext ctx = ..;
//获取到与 ChannelHandlerContext相关联的 ChannelPipeline 的引用
ChannelPipeline pipeline = ctx.pipeline();
//通过 ChannelPipeline 写入缓冲区
pipeline.write(Unpooled.copiedBuffer("Netty in Action",CharsetUtil.UTF_8));

上面两段代码中的事件流是一样的。
重要的是,虽然被调用的 Channel 或 ChannelPipeline 上的 write()方法将一直传播事件通过整个 ChannelPipeline,但是在 ChannelHandler 的级别上,事件从一个 ChannelHandler到下一个 ChannelHandler 的移动是由ChannelHandlerContext 上的调用完成的。

从 ChannelPipeline 中的某个特定点开始传播事件:

  1. 为了减少将事件传经对它不感兴趣的 ChannelHandler 所带来的开销。
  2. 为了避免将事件传经那些可能会对它感兴趣的 ChannelHandler。

要想调用从某个特定的 ChannelHandler 开始的处理过程,必须获取到在(ChannelPipeline)该 ChannelHandler 之前的 ChannelHandler 所关联的 ChannelHandlerContext。这个 ChannelHandlerContext 将调用和它所关联的 ChannelHandler 之后的ChannelHandler

用法如下:

ChannelHandlerContext ctx = ..;
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

消息将从下一个 ChannelHandler 开始流经 ChannelPipeline,绕过了所有前面的 ChannelHandler

6.3.2 ChannelHandler和ChannelHandlerContext高级用法

如下面这段代码,可以通过调用 ChannelHandlerContext 上的pipeline()方法来获得被封闭的 ChannelPipeline 的引用。这使得运行时得以操作ChannelPipeline 的 ChannelHandler,可以利用这一点来实现一些复杂的设计。例如,可以通过将 ChannelHandler 添加到 ChannelPipeline 中来实现动态的协议切换。


ChannelHandlerContext ctx = ..;
//获取到与 ChannelHandlerContext相关联的 ChannelPipeline 的引用
ChannelPipeline pipeline = ctx.pipeline();
//通过 ChannelPipeline 写入缓冲区
pipeline.write(Unpooled.copiedBuffer("Netty in Action",CharsetUtil.UTF_8));

另一种高级的用法是缓存到 ChannelHandlerContext 的引用以供稍后使用,这可能会发
生在任何的 ChannelHandler 方法之外,甚至来自于不同的线程。

//代码清单 6-9 缓存到 ChannelHandlerContext 的引用
public class WriteHandler extends ChannelHandlerAdapter {
        private ChannelHandlerContext ctx;
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            //存储ChannelHandlerContext的引用,以供稍后使用
            this.ctx = ctx;
        }
        //使用之前存储的引用来发送消息
        public void send(String msg) {
            ctx.writeAndFlush(msg);
        }
    }

因为一个 ChannelHandler 可以从属于多个 ChannelPipeline,所以它也可以绑定到多个 ChannelHandlerContext 实例。
对于这种用法,在多个 ChannelPipeline 中共享同一 个 ChannelHandler,对应的 ChannelHandler 必须要使用@Sharable 注解标注

//代码清单 6-10 可共享的 ChannelHandler
@Sharable
    public class SharableHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            System.out.println("Channel read message: " + msg);

            //记录方法调用,并转发给下一个 ChannelHandler
            ctx.fireChannelRead(msg);
        }
    }


这段代码的问题在于它拥有状态,即用于跟踪方法调用次数的实例变量count。(当然,这个简单的问题可以通过使channelRead()方法变为同步方法来修正。)

    1. 为何要共享同一个ChannelHandler
      在多个ChannelPipeline中安装同一个ChannelHandler的一个常见的原因是用于收集跨越多个 Channel 的统计信息。
    1. 只应该在确定了你的 ChannelHandler 是线程安全的时才使用@Sharable 注解

6.4 异常处理

Netty提供几种方式用于处理入站或者出站处理过程中所抛出的异常。

6.4.1 处理入站异常

    1. 入站事件发生异常时,从异常发生的ChannelHandler开始,沿着ChannelPipeline链向后传播。前面的ChannelHandler中的exceptionCaught(...)不会被执行。
    1. 如果想处理入站异常,则需要重写方法exceptionCaught(...)
    1. 建议在ChannelPipeline链的尾部,添加处理入站异常的ChannelHandler

6.4.2 处理出站异常

出站异常的处理与入站异常截然不同。出站异常通过异步通知机制实现:

    1. 每个出站操作都将返回一个ChannelFuture。注册到ChannelFuture的ChannelFutureListener将在操作完成后通知调用方操作成功还是出错了
    1. 几乎所有的ChannelOutboundHandler上的方法都会传入一个ChannelPromise的实例。ChannelPromise提供立即通知的可写方法:setSuccess()/setFailure(Throwable cause),通知调用方操作完成结果。
第一种方法(6.4.2.1)代码实现:
//添加 ChannelFutureListener 到 ChannelFuture
public static void addingChannelFutureListener(ChannelHandlerContext ctx){
    Channel channel = ctx.channel();
    ByteBuf someMessage = Unpooled.buffer();
    //...
    ChannelFuture future = channel.write(someMessage);
    future.addListener((ChannelFuture f) -> {
        if (f.isSuccess()) {
            // operation success.
            System.out.println("success.");
        }else {
            // operation fail
            f.cause().printStackTrace();
        }
    });
}
第二种方法(6.4.2.2)代码实现:
//将 ChannelFutureListener 添加到即将作为参数传递给 ChannelOutboundHandler 的方法的 ChannelPromise
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    ctx.write(msg, promise);
    promise.addListener((ChannelFuture f) -> {
        if (f.isSuccess()) {
            // operation success.
            System.out.println("success.");
        }else {
            // operation fail
            f.cause().printStackTrace();
        }
    });
}

对于细致的异常处理,你可能会发现,在调用出站操作时添加 ChannelFutureListener 更合适,如代码1 所示。而对于一般的异常处
理,你可能会发现,代码2 所示的自定义的 ChannelOutboundHandler 实现的方式更加的简单。

如果 ChannelOutboundHandler 本身抛出了异常会发生什么呢?在这种情况下,Netty 本身会通知任何已经注册到对应 ChannelPromise的监视器。

posted @ 2022-02-23 20:39  economies  阅读(113)  评论(0)    收藏  举报