Netty 框架总结「ChannelHandler 及 EventLoop」

3,024 阅读6分钟

「博客搬家」 原地址: 简书 原发表时间: 2017-05-05

学习了一段时间的 Netty,将重点与学习心得总结如下,本文主要总结ChannelHandler 及 EventLoop 的知识点和基本用法,本文章节排序参照《Netty in Action》的章节排序。

以下内容主要参考「并发编程网」的 《Netty in Action》中文版 以及《Netty in Action》原版图书,辅助参考 Essential Netty in Action 《Netty 实战(精髓)》 以及 Netty 官网的 Netty 4.1 JavaDoc

6. ChannelHandler 和 ChannelPipeline

一个 Channel 正常的生命周期如下图所示。随着状态发生变化,相应的 event 产生。这些 event 被转发到 ChannelPipeline 中的 ChannelHandler 来采取相应的操作。

Channel状态模型

6.1 ChannelHandler

ChannelHandler 有两个重要的子接口:

  • 「ChannelInboundHandler」处理输入数据和所有类型的状态变化
  • 「ChannelOutboundHandler」处理输出数据,可以拦截所有操作

6.1.1 ChannelInboundHandler

下表列出接口 ChannelInboundHandler 的方法。当收到数据或相关 Channel 的状态改变时,这些方法被调用,这些方法和Channel的生命周期密切相关

方法 描述
channelRegistered 当一个Channel注册到EventLoop上,可以处理I/O时被调用
channelUnregistered 当一个Channel从它的EventLoop上解除注册,不再处理I/O时被调用
channelActive 当Channel变成活跃状态时被调用;Channel是连接/绑定、就绪的
channelInactive 当Channel离开活跃状态,不再连接到某个远端时被调用
channelReadComplete 当Channel上的某个读操作完成时被调用
channelRead 当从Channel中读数据时被调用

6.1.2 ChannelOutboundHandler

输出的操作和数据由 ChannelOutBoundHandler 处理。它的方法可以被 Channel,ChannelPipeline 和 ChannelHandlerContext 调用,子接口 ChannelOutboundHandler 的主要方法如下:

方法 描述
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 请求绑定 Channel 到一个本地地址
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) 请求连接 Channel 到远端
disconnect(ChannelHandlerContext, ChannelPromise) 请求从远端断开 Channel
close(ChannelHandlerContext,ChannelPromise) 请求关闭 Channel
deregister(ChannelHandlerContext, ChannelPromise) 请求 Channel 从它的 EventLoop 上解除注册
read(ChannelHandlerContext) 请求从 Channel 中读更多的数据
flush(ChannelHandlerContext) 请求通过 Channel 刷队列数据到远端
write(ChannelHandlerContext,Object, ChannelPromise) 请求通过 Channel 写数据到远端

6.1.3 ChannelHandler 适配器类

ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 这两个适配器类分别提供了 ChannelInboundHandler 和 ChannelOutboundHandler 的基本实现,它们继承了共同的父接口 ChannelHandler 的方法,扩展了抽象类 ChannelHandlerAdapter。

ChannelHandlerAdapter类层级关系

  • ChannelHandlerAdapter 提供了工具方法 isSharable()。如果类实现带 @Sharable 注解,那么这个方法就会返回 true,意味着这个对象可以被添加到多个 ChannelPipeline 中。

  • ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 中的方法调用相关 ChannelHandlerContext 中的等效方法,因此将事件转发到管道中的下一个ChannelHandler。

6.1.4 ChannelFuture 和 ChannelPromise

  • ChannelPromise 是 ChannelFuture 的子接口
  • 而 ChannelFuture 是不可变对象
  • ChannelPromise 定义了可写的方法,比如 setSuccess(), setFailure()

6.1.5 释放资源

1. 输入方向「Inbound」 当一个 ChannelInboundHandler 实现类重写 channelRead() 方法时,它要负责释放 ByteBuf 相关的内存。可使用 Netty 提供的工具方法:

    ReferenceCountUtil.release(「ByteBuf 的对象」)

更简单的,可使用子类 SimpleChannelInboundHandler ,一条消息在被 ChannelRead0() 读取后,会被自动释放资源,此时任何对消息的引用都会变成无效,所以不能保存这些引用待后来使用。

2. 输出方向「Outbound」 在输出方向,如果处理一个 write() 操作并且丢弃一条消息(没有写入 Channel),就应该负责释放这条消息。

@ChannelHandler.Sharable public 
class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    ReferenceCountUtil.release(msg);  //使用 ReferenceCountUtil.release(...) 释放资源
    promise.setSuccess();  //通知 ChannelPromise 数据已经被处理
}

如果一个消息被“消费”或者丢弃,没有送到 ChannelPipeline 中的下一个 ChannelOutboundHandler,用户就要负责调用 ReferenceCountUtil.release()。如果消息到达了真正的传输层,在它被写到 Socket 中或者 Channel 关闭时,会被自动释放,用户不用管。

6.2 ChannelPipeline 接口

  • 每个新创建的 Channel 都会分配一个新的 ChannelPipeline,Channel 不可以更换或解除当前的 ChannelPipeline,在 Netty 组件的整个生命周期中这个关系是固定的。

  • 一个 ChannelPipeline 可看成是一串 ChannelHandler 实例,拦截穿过 Channel 的输入输出 event。

  • 根据来源,一个 event 可以被一个 ChannelInboundHandler 或 ChannelOutboundHandler 处理。接下来,通过调用 ChannelHandlerContext 的方法,event 会被转发到下一个同类型的 handler。

6.2.1 ChannelHandlerContext

  • 通过 ChannelHandlerContext,一个 handler 可以通知 ChannelPipeline 中的下一个ChannelHandler,甚至动态改动下一个ChannelHandler 所属的 ChannelPipeline。

  • ChannelPipeline 主要由一系列 ChannelHandler 组成的。ChannelPipeline 提供在 ChannelPipeline 中传送 event 的方法。

  • ChannelHandlerContext 的一些方法和其他类(Channel 和 ChannelPipeline)的方法名字相似,但是 ChannelHandlerContext 的方法采用了更短的 event 传递路程。我们应该尽可能利用这一点来实现最好的性能。

  • 如果你在 Channel 或者 ChannelPipeline 实例上调用这些方法,它们的调用会穿过整个 pipeline。而在 ChannelHandlerContext 上调用的同样的方法,仅仅从当前 ChannelHandler 开始,走到 pipeline 中下一个可以处理这个 event 的 ChannelHandler。

ChannelPipeline 和 ChannelHandlers

「本节参考」 第六章 ChannelHandler 和 ChannelPipeline

7. EventLoop 和 EventLoopGroup

7.1 Java 基本的线程池模式

  • 从池中空闲的线程中选出一个,分配一个提交的task「一个Runnable的实现」
  • 当task完成,线程返回池中,等待复用「下一次task分配」

7.2 EventLoop「事件循环」

  • EventLoop 始终由一个线程驱动
  • 一个 EventLoop 可以被指派来服务多个 Channel
  • 一个 Channel 只拥有一个 EventLoop

task (Runnable或Callable) 可以直接提交到 EventLoop 实现即刻或者延后执行。根据配置和可用的CPU核,可以创建多个 EventLoop 来优化资源利用。

一个 event 的本质决定了它将如何被处理;它可能从网络协议栈传送数据到你的应用,或者反过来,或者做一些完全不一样的事情。但是 event 处理逻辑必须足够通用和灵活,来对付所有可能的情况。

所以,在 Netty 4,所有的 I/O 操作和 event 都是由分配给 EventLoop 的那一个 Thread 来处理的。Netty 4 采用的线程模型,在同一个线程的 EventLoop 中处理所有发生的事。

7.3 EventLoopGroup

  • EventLoopGroup 负责分配 EventLoop 到新创建的 Channel
  • 异步实现只用了很少 EventLoop,这几个 EventLoop 被所有 Channel 共享
  • 一但 Channel 被指派了一个 EventLoop,在它的整个生命周期过程中,都会用这个 EventLoop

针对非阻塞传输的EventLoop分配

为 Channel 的 I/O 和 event 提供服务的 EventLoop 都包含在一个 EventLoopGroup 中。EventLoop 创建和分配的方式根据传输实现的不同而有所不同。

异步实现只用了很少几个 EventLoop(和它们关联的线程),在目前 Netty 的模型中,这几个 EventLoop 被所有 Channel 共享。这让很多 Channel 被最少数量的线程服务,而不是每个 Channel 分配一个线程。

EventLoopGroup 负责分配一个 EventLoop 到每个新创建的 Channel。在目前的实现中,采用循环 (round-robin) 策略可以满足一个平衡的分配,同一个 Eventloop 还可能会被分配到多个 Channel。

「本节参考」 第七章 EventLoop和线程模型

参考链接

  1. 《Netty in Action》中文版
  2. Essential Netty in Action 《Netty 实战(精髓)》
  3. Netty 4.1 JavaDoc