Netty 框架总结「ChannelHandler 及 Even
学习了一段时间的 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。
「本节参考」 第六章 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
为 Channel 的 I/O 和 event 提供服务的 EventLoop 都包含在一个 EventLoopGroup 中。EventLoop 创建和分配的方式根据传输实现的不同而有所不同。
异步实现只用了很少几个 EventLoop(和它们关联的线程),在目前 Netty 的模型中,这几个 EventLoop 被所有 Channel 共享。这让很多 Channel 被最少数量的线程服务,而不是每个 Channel 分配一个线程。
EventLoopGroup 负责分配一个 EventLoop 到每个新创建的 Channel。在目前的实现中,采用循环 (round-robin) 策略可以满足一个平衡的分配,同一个 Eventloop 还可能会被分配到多个 Channel。
「本节参考」 第七章 EventLoop和线程模型