Netty组件(一)—— Channel、EventLoop和ChannelFuture
Netty组件(一)—— Channel、EventLoop和ChannelFuture
在从NIO编程到Netty的使用中,我们简单介绍了一下Netty,并写了一个很简单的小例子,这里我们就来详细介绍一个之前例子中用到的一些Netty的组件。
Channel
基本的I/O 操作bind()、connect()、read()和write()
依赖于底层网络传输所提供的原语。在基于Java 的网络编程中,其基本的构造是class Socket。Netty 的Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
每个Channel 都将会被分配一个ChannelPipeline 和ChannelConfig。ChannelConfig 包含了该Channel 的所有配置设置,并且支持热更新。
由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为Comparable接口的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。
Channel的生命周期状态
ChannelUnregistered
Channel 已经被创建,但还未注册到EventLoopChannelRegistered
Channel 已经被注册到了EventLoopChannelActive
Channel 处于活动状态(已经连接到它的远程节点),可以接收和发送数据了ChannelInactive
Channel 没有连接到远程节点
Channel常用方法
-
eventLoop()
返回分配给Channel 的EventLoop -
pipeline()
返回分配给Channel 的ChannelPipeline -
isActive()
如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。 -
localAddress()
返回本地的SokcetAddress -
remoteAddress()
返回远程的SocketAddress -
write()
将数据写到远程节点。这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷 -
flush()
将之前已写的数据冲刷到底层传输,如一个Socket -
writeAndFlush()
一个简便的方法,等同于调用write()并接着调用flush()
其中最后三个方法write()、flush()、writerAndFlush()
是在其父接口中定义的
EventLoop
EventLoop定义了Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。io.netty.util.concurrent 包构建在JDK 的java.util.concurrent 包上。一个EventLoop 将由一个永远都不会改变的Thread 驱动,同时任务(Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行。
根据配置和可用核心的不同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个Channel,如下
- 一个EventLoopGroup 包含一个或者多个EventLoop
- 一个EventLoop 在它的生命周期内只和一个Thread 绑定
- 所有由EventLoop 处理的I/O 事件都将在它专有的Thread 上被处理
- 一个Channel 在它的生命周期内只注册于一个EventLoop
- 一个EventLoop 可能会被分配给一个或多个Channel
异步传输的实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在上述的线程模型中,它们可能会被多个Channel 所共享。这使得可以通过尽可能少量的Thread 来支撑大量的Channel,而不是每个Channel 分配一个Thread。
EventLoopGroup 负责为每个新创建的Channel 分配一个EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel。一旦一个Channel 被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的Thread)。
另外需要注意的是,EventLoop的分配方式对ThreadLocal的使用的影响。因为一个EventLoop 通常会被用于支撑多个Channel,所以对于所有相关联的Channel 来说,ThreadLocal 都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。但是在一些无状态的上下文中,它仍然可以被用于在多个Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。
在内部,当提交任务到如果(当前)调用线程正是支撑EventLoop的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。
Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法parent()
。在Netty 4中,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。
从上述的类图中发现其实现了ScheduledExecutorService接口,即可以调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了5 分钟之后触发的任务。一个常见的方法是,发送心跳消息到远程节点,以检查连接是否仍然还活着。如果没有响应,你便知道可以关闭该Channel了。
ChannelFuture
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。可以将ChannelFuture 看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。
如在之前的例子中,我们服务器在接受到数据后,再将数据重新写回后,就关闭了客户端的连接,如下: