本文参考

本篇文章是对《Netty In Action》一书第六章"ChannelHandler和ChannelPipeline",主要内容为ChannelHandler API 、ChannelPipeline API、检测资源泄漏和异常处理

这一篇文章讲到的内容,在前面几篇文章中或多或少已有涉及,那些重复的部分算作是回顾吧

Channel的生命周期

registered(Channel已经被注册到EventLoop) -> active(Channel处于活动状态,已经连接到远程节点,可以接收和发送数据) - > inactive(Channel没有连接到远程节点)-> unregistered(Channel已经被创建,但还未注册到EventLoop或已从EventLoop中注销)

当这些状态发生改变时,将会生成对应的事件,这些事件将会被转发给ChannelPipeline中的ChannelHandler,如ChannelInboundHandler中的channelRegistered()、channelActive()、channelInactive()、channelUnregistered()方法

ChannelHandler生命周期

handlerAdded() -> 当把ChannelHandler添加到ChannelPipeline中时被调用

handlerRemoved() -> 当从ChannelPipeline中移除ChannelHandler时被调用

exceptionCaught() -> 当处理过程中在ChannelPipeline中有错误产生时被调用

在ChannelHandler 被添加到ChannelPipeline中或者被从ChannelPipeline中移除时会调用这些操作

ChannelInboundHandler接口 API

ChannelHandler which adds callbacks for state changes. This allows the user to hook in to state changes easily

下面是ChannelInboundHandler的生命周期方法。这些方法将会在数据被接收时或者与其对应的Channel状态发生改变时被调用。正如Channel生命周期中所提到的,这些方法和Channel的生命周期密切相关

我们在Netty客户端 / 服务端一文中提到由一方发送的消息可能会被分块接收,channelRead()方法可能会被调用多次,channelReadComplete()方法仅会被调用一次。因此在服务端的业务实现中,我们在channelRead()方法内仅使用write()方法,将接收到的消息写给发送者,而不冲刷出站消息,而是在channelReadComplete()方法内使用writeAndFlush()方法冲刷消息

当某个ChannelInboundHandler的实现重写channelRead()方法时,它将负责显式地释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法 ReferenceCountUtil.release()。该操作已经在SimpleChannelInboundHandler的channelRead()方法中实现,我们只需重载channelRead0()方法,所以也就不能够存储指向任何消息的引用供将来使用,因为这些引用都将会失效

ChannelOutboundHandler 接口 API

ChannelHandler which will get notified for IO-outbound-operations

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

ChannelOutboundHandler中的大部分方法都需要一个 ChannelPromise参数,以便在操作完成时得到通知。ChannelPromise是ChannelFuture的一个子类,其定义了一些可写的方法,如setSuccess()和setFailure(),当一个 Promise 被完成之后,其对应的 Future 的值便 不能再进行任何修改了

ChannelHandlerAdapter适配器

Skeleton implementation of a ChannelHandler

ChannelHandlerAdapter抽象类实现了Channel接口的isSharable()方法,如果其对应的实现被标 注为 Sharable,那么这个方法将返回 true,表示它可以被添加到多个 ChannelPipeline 中

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

资源管理

每当通过调用 ChannelInboundHandler.channelRead()或者 ChannelOutboundHandler.write()方法来处理数据时,都需要确保没有任何的资源泄漏

若ChannelInBoundHandlerAdapter的channelRead()方法直接消费入站消息,不会通过调用 ChannelHandlerContext.fireChannelRead() 方法将入站消息转发给下一个ChannelInboundHandler时,应当通过ReferenceCountUtil.release(msg);释放资源

若ChannelOutBoundHandlerAdapter的write()方法的响应操作完成,不会传递给 ChannelPipeline 中的下一个 ChannelOutboundHandler时,注意不仅要通过ReferenceCountUtil.release(msg);释放资源,还要通知 ChannelPromise数据已经被处理,即promise.setSuccess();

Netty提供了class ResourceLeakDetector 对应用程序的缓冲区分配做大约1%的采样来检测内存泄露

泄露检测级别可以通过将下面的 Java 系统属性设置为表中的一个值来定义

java -Dio.netty.leakDetectionLevel=ADVANCED

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1 #1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133) ...

ChannelPipeline操作ChannelHandler API

在前面的学习中我们已经知道ChannelPipeline是一个拦截流经Channel的入站和出站事件的ChannelHandler 实例链

每一个新创建的Channel都将会被分配一个新的ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。

但是ChannelHandler 可以通过添加、删除或者替换其他的 ChannelHandler 来实时地修改 ChannelPipeline的布局(也可以将它自己从ChannelPipeline中移除)

ChannelPipeline pipeline = channelInstance.pipeline();
//
创建一个 FirstHandler 的实例
FirstHandler firstHandler = new FirstHandler();
//将该实例作为"handler1"添加到ChannelPipeline 中
pipeline.addLast("handler1", firstHandler);
//将一个 SecondHandler的实例作为"handler2"添加到 ChannelPipeline的第一个槽中
// 这意味着它将被放置在已有的"handler1"之前
pipeline.addFirst("handler2", new SecondHandler());
//将一个 ThirdHandler 的实例作为"handler3"添加到 ChannelPipeline 的最后一个槽中
pipeline.addLast("handler3", new ThirdHandler());
//通过名称移除"handler3"
pipeline.remove("handler3");
//通过引用移除FirstHandler(它是唯一的,所以不需要它的名称)
pipeline.remove(firstHandler);
// SecondHandler("handler2")替换为 FourthHandler:"handler4"
pipeline.replace("handler2", "handler4", new FourthHandler());

除了这些操作,还有别的通过类型或者名称来访问ChannelHandler的方法。

ChannelPipeline 入站事件API

ChannelPipeline 出站事件API

ChannelHandler 在ChannelPipeline的共享

有时我们需要收集跨越多个Channel的统计信息,使一个 ChannelHandler可以从属于多个 ChannelPipeline,所以它也可以绑定到多个 ChannelHandlerContext 实例。对于这种用法指在多个 ChannelPipeline 中共享同一个 ChannelHandler,对应的 ChannelHandler 必须要使用@Sharable注解标注;否则,试图将它添加到多个 ChannelPipeline 时将会触发异常。显而易见,为了安全地被用于多个并发的 Channel(即连接),这样的 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);
  }
}

下面是一种错误用法

@Sharable
public class UnsharableHandler extends ChannelInboundHandlerAdapter {

  private int count;

  @Override

  public void channelRead(ChannelHandlerContext ctx, Object msg) {

    // count 字段的值加 1

    count++;

    //记录方法调用,并转发给下一个ChannelHandler

    System.out.println("inboundBufferUpdated(...) called the " + count + " time");

    ctx.fireChannelRead(msg);
  }
}

因为拥有"状态",即用于跟踪方法调用次数的实例变量count。将这个类的一个实例添加到ChannelPipeline将极有可能在它被多个并发的Channel访问时导致问题

入站异常捕获机制

在第二篇"Netty客户端 / 服务端概览"中我们就已经接触了异常处理,ChannelHandler.exceptionCaught()的默认实现是简单地将当前异常转发给 ChannelPipeline中的下一个ChannelHandler

如果异常到达了ChannelPipeline的尾端,它将会被记录为未被处理

要想定义自定义的处理逻辑,你需要重写exceptionCaught()方法。然后你需要决定是否需要将该异常传播出去

public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {

  @Override

  public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) {

    cause.printStackTrace();

    ctx.close();
  }
}

出站异常捕获机制

每个出站操作都将返回一个ChannelFuture,注册到ChannelFuture的ChannelFutureListener将在操作完成时被通知该操作是成功还是出错

添加 ChannelFutureListener 只需要调用 ChannelFuture 实例上的 addListener (ChannelFutureListener)方法,例如,调用出站操作(如write()方法)所返回的ChannelFuture上的addListener()方法

ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {

  @Override

  public void operationComplete(io.netty.channel.ChannelFuture f) {

    if (!f.isSuccess()) {

      f.cause().printStackTrace();

      f.channel().close();
    }
  }
});

几乎所有的 ChannelOutboundHandler 上的方法都会传入一个 ChannelPromise 的实例。作为 ChannelFuture 的子类,ChannelPromise 也可以被分配用于异步通知的监听器。但是,ChannelPromise还具有提供立即通知的可写方法 -> setSuccess()方法和setFailure()方法

public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {

  @Override

  public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {

    promise.addListener(new ChannelFutureListener() {

      @Override

      public void operationComplete(ChannelFuture f) {

        if (!f.isSuccess()) {

          f.cause().printStackTrace();

          f.channel().close();
        }
      }
    });
  }
}

对于细致的异常处理,在调用出站操作时添加 ChannelFutureListener 更合适,所以我们一般采用第一种方法

Netty学习摘记 —— 再谈ChannelHandler和ChannelPipeline的更多相关文章

  1. Netty学习摘记 —— 再谈引导

    本文参考 本篇文章是对<Netty In Action>一书第八章"引导"的学习摘记,主要内容为引导客户端和服务端.从channel内引导客户端.添加ChannelHa ...

  2. Netty学习摘记 —— 再谈EventLoop 和线程模型

    本文参考 本篇文章是对<Netty In Action>一书第七章"EventLoop和线程模型"的学习摘记,主要内容为线程模型的概述.事件循环的概念和实现.任务调度和 ...

  3. Netty学习摘记 —— 初步认识Netty核心组件

    本文参考 我在博客内关于"Netty学习摘记"的系列文章主要是对<Netty in action>一书的学习摘记,文章中的代码也大多来自此书的github仓库,加上了一 ...

  4. Netty学习摘记 —— 心跳机制 / 基于分隔符和长度的协议

    本文参考 本篇文章是对<Netty In Action>一书第十一章"预置的ChannelHandler和编解码器"的学习摘记,主要内容为通过 SSL/TLS 保护 N ...

  5. Netty学习摘记 —— 初识编解码器

    本文参考 本篇文章是对<Netty In Action>一书第十章"编解码器框架"的学习摘记,主要内容为解码器和编码器 编解码器实际上是一种特殊的ChannelHand ...

  6. Netty学习摘记 —— Netty客户端 / 服务端概览

    本文参考 本篇文章是对<Netty In Action>一书第二章"你的第一款 Netty 应用程序"的学习摘记,主要内容为编写 Echo 服务器和客户端 第一款应用程 ...

  7. Netty学习摘记 —— 简单WEB聊天室开发

    本文参考 本篇文章是对<Netty In Action>一书第十二章"WebSocket"的学习摘记,主要内容为开发一个基于广播的WEB聊天室 聊天室工作过程 请求的 ...

  8. Netty学习摘记 —— 预置SSL / HTTP / WebSocket编解码器

    本文参考 本篇文章是对<Netty In Action>一书第十一章"预置的ChannelHandler和编解码器"的学习摘记,主要内容为通过 SSL/TLS 保护 N ...

  9. Netty学习摘记 —— 单元测试

    本文参考 本篇文章是对<Netty In Action>一书第九章"单元测试"的学习摘记,主要内容为使用特殊的 Channel 实现--EmbeddedChannel来 ...

随机推荐

  1. 网络之IP地址、子网掩码、网关关联

    IP地址?子网掩码? 网关?我们经常混淆这些知识,同时面试的时候又容易被问.下面我们就一个一个的来介绍他们的区别和用途. 网络无处不在,深深影响着我们的生活.而下面几点知识是我们在网络学习中经常遇到的 ...

  2. C# init用法

    init是什么意思? init就 modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) 类型的缩写 modreq ...

  3. starter

    传统的 Spring 项目想要运行,不仅需要导入各种依赖,还要对各种 XML 配置文件进行配置,十分繁琐,但 Spring Boot 项目在创建完成后,即使不编写任何代码,不进行任何配置也能够直接运行 ...

  4. Java课程设计---学生信息管理系统需求分析及总体设计

    按照软件工程实践的原则,开发大型程序需要经历需求分析.总体设计.详细设计.编码实现.系统测试.系统维护等几个阶段. 1.需求分析 本阶段是整个软件开发过程中最重要的环节.通过了解实际运行的系统或与用户 ...

  5. netty系列之:EventLoop,EventLoopGroup和netty的默认实现

    目录 简介 EventLoopGroup和EventLoop EventLoopGroup在netty中的默认实现 EventLoop在netty中的默认实现 总结 简介 在netty中不管是服务器端 ...

  6. 矩池云上nvidia opencl安装及测试教程

    本教程租用的是2080ti,3.7多框架镜像. 添加nvidia-cuda的阿里源 curl -fsSL https://mirrors.aliyun.com/nvidia-cuda/ubuntu18 ...

  7. 『现学现忘』Docker基础 — 16、Docker中的基本概念和底层原理

    目录 1.Docker的底层原理 2.Docker中常用的基本概念 3.run命令的运行流程 4.为什么Docker比VM快 Docker架构图: 我们依照Docker架构图进行Docker基础概念的 ...

  8. 3、myql的逻辑架构和sql的执行流程

    msyql逻辑架构 逻辑架构的解析 逻辑架构图如下(序号代表的是:服务器处理客户端请求的流程) 1.1connectors connectors是指使用不同语言的客户端与mysql server服务器 ...

  9. ArcMap操作随记(10)

    1.基于点生成辐射线 [缓冲区]→[构造视线] 2.求算点集中于剩余点距离总和最小的点 [构造视线]→[计算几何]→[汇总] 3.关于空间参考,关于投影 ①横轴墨卡托投影 "等角横轴切圆柱投 ...

  10. 程序设计基础·Java学习笔记·面向对象(上)

    Java程序设计基础之面向对象(上) (自适应学习进度而进行记录的笔记,希望有一些小小的用处吧(^∀^●)ノシ) (新人上路,望多指教,如有错误,望指正,万分感谢(o゚v゚)ノ) 目录 一.面向对象 ...