Netty的核心组件
- Channel
- 回调
- Future
- 事件和ChannelHandler
Channel
Channel是JavaNIO的基本构造。
它代表一个到实体的开放连接,如读操作和写操作。
目前,可以把Channel看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
回调
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。
Netty的内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个interface ChannelHandler的实现处理。
Future
Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个一步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
JDK预置了interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。所以Netty提供了它自己的实现-ChannelFuture,用于在异步操作的时候使用。
ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功完成了还是出错了。如果是后者,我们可以检索产生一个Throwable。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。
每个Netty的出站IO操作都将返回一个ChannelFuture;也就是说,他们都不会阻塞。
需要注意的是,对错误的处理完全取决于你、目标,当然也包括目前任何对于特定类型的错误加以的限制。例如,连接失败,你可以尝试重新建立连接或者建立一个到另一个远程节点的连接。
如果你把ChannelFutureListener看作是回调的一个更加精细的版本,那么你是对的。事实上,回调和Future是相互补充的机制;它们相互结合,构成了Netty本身的关键构建之一。
事件和ChannelHandler
Netty使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:
- 记录日志
- 数据转换
- 流控制
- 应用程序逻辑
Netty是一个网络编程框架,所以事件是按照他们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已被激活或者连接失活
- 数据读取
- 用户事件
- 错误事件
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭远程节点的连接。
- 将数据写到或者冲刷到套接字。
每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。
Netty的ChannelHandler为处理器提供了基本的抽象。Netty提供了大量预定义的可以开箱即用的ChannelHandler实现,包括用于各种协议的ChannelHandler。在内部,ChannelHandler自己也使用了事件和Future,使得他们也成为了你的应用程序将使用的相同抽象的消费者。
1、Future、回调和ChannelHandler:Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件派发到ChannelHandler的方法则发生在更深的层次上。结合在一起,这些元素就提供了一个处理环境,使你的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。这也是Netty的设计方式的一个关键目标。
拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的Future。这使得连接操作变得既简单又高效。
2、选择器、事件和EventLoop
Netty通过触发事件将Selector从应用程序中抽象出来,消除了所有本来需要手动编写的派发代码。在内部,将会为每个Channel分配一个EventLoop,用以处理所有事件,包括:
- 注册感兴趣的事件;
- 将事件派发给ChannelHandler;
- 安排进一步的动作;
EventLoop本身只由一个线程驱动,其处理了一个Channel的所有IO事件,并且在该EventLoop的整个生命周期内都不会改变。
Netty的组件和设计
从高层次的角度来看,Netty解决了两个相应的关注领域,我们可以将其大致标记为技术的和体系结构的。首先,它的基于JavaNIO的异步的和事件驱动的实现,保证了高负载下应用程序性能的最大化和可伸缩性。其次,Netty也包含了一组设计模式,将应用程序逻辑从网络层解耦,简化了开发过程,同时也最大限度地提高了可测试性、模块化以及代码的可重用性。
Channel、EventLoop和ChannelFuture
- Channel Socket
- EventLoop 控制流、多线程处理、并发
- ChannelFuture 异步通知
Channel接口
基于的IO操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的源语。在基于Java的网络编程中,其基本的构造是class Socket。Netty的Channel接口所提供的API,大大降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
EventLoop接口
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。
1、使用EventLoopGroup所提供的EventLoop
2、创建Channel
3、将Channel注册到EventLoop
4、整个生命周期内部都使用EventLoop处理IO事件
- 一个EventLoopGroup中包含一个或者多个EventLoop
- 一个EventLoop在它的生命周期内只和一个Thread绑定
- 所有由EventLoop处理的IO事件都将在它专有的Thread上被处理
- 一个Channel在它的生命周期内只注册与一个EventLoop
- 一个EventLoop可能会被分配给一个或者多个Channel
注意,在这种设计中,一个给定的Channel的IO操作都是由相同的Thread执行的,实际上消除了对于同步的需要。
ChannelFuture接口
Netty中所有的IO操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
ChannelHandler和ChannelPipeline
ChannelHandler接口
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler的方法是由网络事件触发的。事实上,ChannelHandler可专门用于几乎任何类型的动作。
ChannelPipeline接口
ChannelPipeline接口为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流API。当Channel被创建时,他会被自动地分配到它专属的ChannelPipeline。
ChannelHandler安装到ChannelPipeline中的过程如下所示:
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中。
- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler。
- ChannelInitializer将他自己从ChannelPipeline中移除。
ChannelHandler是专门为支持广泛的用途而设计的,可以将它看作是处理往来ChannelPipeline事件的任何代码的通用容器。ChannelHandler派生了ChannelInboundHandler和ChannelOutboundHandler接口。
使得事件流经ChannelPipeline是ChannelHandler的工作,他们是在应用程序的初始化或者引导阶段被安装的。这些对象接受事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。他们的执行顺序是由他们被添加的顺序所决定的。实际上、被我们称为ChannelPipeline的是这些ChannelHandler的编排顺序。
从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,反之则称为入站的。
如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,并传递给第一个ChannelInboundHandler。这个ChannelHandler不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终数据将会到达ChannelPipeline的尾端,届时,所有的处理就都结束了。
数据的出站运动在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。
通过使用作为参数传递到每个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler链中的下一个ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。通过调用ChannelHandlerContext上的对应方法,每个都提供了简单的将事件传递给下一个ChannelHandler的方法的实现。最后,你可以重写你所感兴趣的那些方法来扩展这些类。
鉴于出站操作和入站操作是不同的,你可能会想知道如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline中会发生什么。虽然ChannelInboundHandler和ChannelOutboundHandler都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向的类型的两个ChannelHandler之间传递。
当ChannelHandler被添加到ChannelPipeline是,他会被分配一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式,你可以直接写到Channel中,也可以写到ChannelHandler相关的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline的尾端开始流动,而后者将导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。
编码器和解码器
当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换成另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
对于特定的需要,Netty为编码器和解码器提供了不同类型的抽象类。例如,你的应用程序可能使用一种中间格式,而不需要立即将消息转换成字节。通常来说,这些基类的名称将类似于ByteToMessageDecoder或MessageToByteEncoder。你将会发现对于出站数据来说,channelRead方法事件被重写了。对于每个从入站Channel读取的消息,这个方法都会将被调用。随后他将调用预置解码器的decode方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
出站消息的模式是相反方向的:编码器将消息转换为字节,并将他们转发给下一个ChannelOutboundHandler。
引导
Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
通常来说,我们把前面的用例称作引导了一个服务器,后面的用例称作引导了一个客户端。
因此,有两种类型的引导:一种用于客户端(Bootstrap),而另一种用于服务器(ServerBootstrap)
引导一个客户端只要一个EventLoopGroup,但是一个ServerBootstrap则需要两个。因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端的连接的Channel。与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接受,第二个EventLoopGroup就会给他的Channel分配一个EventLoop。
传输API
传输API的核心是interface Channel,它被用于所有的IO操作。
每个Channel都将会被分配一个ChannelPipeline和ChannelConfig。ChannelConfig包含了该Channel的所有配置,并且支持热更新。由于特定的传输可能具有独特的设置,所有他可能会实现一个ChannelConfig的子类型。
由于Channel是独一无二的,所以为了保证顺序将Channel声明为java.lang.Comparable的一个子接口。因此,如果两个不同的Channel实例都返回了相同的散列码,那么AbstractChannel中的compareTo()方法将会抛出一个Error。
ChannelPipeline持有所有将应用于出站和入站数据以及事件的ChannelHandler实例,这些ChannelHandler实现了应用程序用于处理状态变化以及数据处理逻辑。
ChannelHandler的典型用途包括:
- 将数据从一种格式转换为另一种格式;
- 提供异常通知;
- 提供Channel变为活动的或者非活动的通知;
- 提供当Channel注册到EventLoop或者从EventLoop注销的通知;
- 提供有关用户自定义事件的通知;
NIO 非阻塞IO
NIO提供了一个所有IO操作的全异步的实现。它利用了自NIO子系统被引入JDK1.4时便可用的基于选择器的API。
选择器的背后的基本概念是充当一个注册表,在那里你将可以请求在Channel的状态发生变化时得到通知。可能得状态变化有:
- 新的Channel已被接受并且就绪
- Channel连接已经完成
- Channel有已经就绪的可供读取的数据
- Channel可用于写数据
选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。
ByteBuf
网络数据的基本单位总是字节。JavaIO提供了ByteBuffer的字节容器,但是这个类使用起来过于复杂,而且有些繁琐。
Netty的ByteBuffer替代品是ByteBuf,一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。
ByteBuf的API
Netty的数据处理API通过两个组件暴露-abstract class ByteBuf和interface ByteBufHolder。
下面是一些ByteBufAPI的优点:
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长
- 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
ByteBuf类-Netty的数据容器
它是如何工作的
ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writeIndex也会被递增。
名称以read和write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或get开头的操作则不会。
字节级操作
随机访问索引
ByteBuf的索引都是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity()-1。
需要注意的是,使用那些需要一个索引值参数的方法之一来访问数据既不会改变readerIndex也不会改变writerIndex。如果有需要,也可以通过readerIndex或者writerIndex来手动移动这两者。
可读字节
ByteBuf的可读字节分段存储了实际数据。新分配的、新包装的或者复制的缓冲区的默认的readerIndex值为0,。任何名为read或者skip开头的操作都将索引或者跳过位于当前readerIndex的数据,并且将它增加已读字节数。
可写字节
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex的默认值为0,。任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。如果写操作目标也是ByteBuf,并且没有指定源索引值,则源缓冲区readerIndex也同样会被增加相同的大小。
ChannelHandler和ChannelPipeline
ChannelHandler
Channel的生命周期
interface Channel定义了一组和ChannelInboundHandlerAPI密切相关的简单但功能强大的状态模型。
状态 | 描述 |
---|---|
ChanelUnregistered | Channel已经被创建,但还未被注册到EventLoop |
ChannelRegistered | Channel已经被注册到EventLoop |
ChannelActive | Channel处于活动状态。他现在可以接收和发送数据了 |
ChannelInactive | Channel没有连接到远程节点 |
ChannelHandler的生命周期
Netty定义了下面两个重要的ChannelHandler子接口:
- ChannelInboundHandler 处理入站数据以及各种状态变化
- ChannelOutboundHandler 处理出站数据并且允许拦截所有的操作