目录
Channel、EventLoop和ChannelFuture
Channel接口
EventLoop接口
ChannelFuture接口
ChannelHandler和ChannelPipeline
ChannelHandler接口
ChannelPipeline接口
更加深入地了解ChannelHandler
编码器和解码器
抽象类SimpleChannelInboundHandler
引导
从高层次的角度来看,Netty解决了两个相应的关注领域,技术的和体系结构的。首先,它的基于java nio的异步的和事件驱动的实现,保证了高负载下应用程序性能的最大化和可伸缩性。其次,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的核心抽象,用于处理连接的生命周期中所发生的事件。下图说明了Channel、EventLoop、Thread以及EventLoopGroup之间的关系。
- 一个EventLoopGroup包含一个或多个EventLoop;一个EventLoop在它的生命周期内只和一个Thread绑定,所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
- 一个Channel在它的生命周期内只注册于一个EventLoop,一个EventLoop可能被分配给一个或多个Channel。
在这种设计中,一个给定Channel的所有I/O操作都是由相同的Thread执行的,实际上消除了对同步的需要。
ChannelFuture接口
Netty中所有的I/O操作都是异步的,因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener方法注册了ChannelFutureListener,以便在某个操作完成时得到通知。
ChannelHandler和ChannelPipeline
ChannelHandler接口
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler的方法是由网络事件触发的。实际上ChannelHandler可专门用于几乎所有类型的动作,例如将数据从一种格式转换为另一种格式,或者处理转换过程中所抛出的异常。
举例来说,ChannelInboundHandler是一个你经常会实现的子接口。这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的应用程序的业务逻辑通常驻留在一个或多个ChannelInboundHandler中。
ChannelPipeline接口
ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会被自动地分配到它专属的ChannelPipeline。
ChannelHandler安装到ChannelPipeline中的过程如下:
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中
- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler
- ChannelInitializer将它自己从ChannelPipeline中移除
ChannerHandler是专门为支持广泛的用途而设计的,可以将它看作是处理往来ChannelPipeline事件(包括数据)的任何代码的通用容器。使得事件流经ChannelPipeline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为ChannelPipeline的是这些ChannelHandler的编排顺序。
入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中。如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipieline的头部开始流动,并被传递给第一个ChannelInboundHandler,然后下一个ChannelInboundHandler,直到ChannelPipeline的尾端。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。
鉴于出站和入站操作是不同的,你可能会想知道如果将两个类别的ChannelHandler都混合添加到一个ChannelPipeline中会发生什么。Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递。
当ChannelHandler被添加到ChannelPipeline时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以写到和ChannelPipeline相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline的尾端开始流动,而后者将导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。
更加深入地了解ChannelHandler
有许多不同类型的ChannelHandler,它们各自的功能主要取决于它们的超类。Netty以适配器的形式提供了大量默认的ChannelHandler实现,其旨在简化应用程序处理逻辑的开发过程。ChannelPipeline中的每个ChannelHandler负责把事件传递给链中的下一个ChannelHandler。这些适配器(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。
接下来,我们将研究3个ChannelHandler的子类型:编码器、解码器和SimpleChannelInboundHandler<T>——ChannelInboundHandlerAdapter的一个子类。
编码器和解码器
当你通过Netty发送或者接收一个消息的时候,就会发生一次数据转换。入站消息会被解码;也就是说从字节转换为另一种格式,通常是一个java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单,网络数据总是一系列的字节。
对应于特定的需要,Netty为编码器和解码器提供了不同类型的抽象类。例如,你的应用程序可能提供了一种中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是它将派生自一个不同的超类。为了确定合适的编解码器类型,你可以应用一个简单的命名规约。
通常来说,这些基类的名称将类似于ByteToMessageDecoder或MessageToByteEncoder。对于特殊的类型,你可能会发现类似于ProtobufEncoder和ProtobufDecoder这样的名称——预置的用来支持Google的Protocol Buffers。
严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化ChannelHandler的创建的适配器一样,所有由Netty提供的编码器/解码器适配器类都实现了ChannelOutboundHandler和ChannelInboundHandler接口。
你将会发现对于入站数据来说,channelRead方法已经被重写了。对于每个入站Channel读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的decode方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelHandler。
出站消息的模式是相反方向的:编码器将消息转为字节,并将它们转发给下一个ChannelOutboundHandler。
抽象类SimpleChannelInboundHandler
最常见的情况是,你的应用程序会利用一个ChannelHandler来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler<T>,其中T是你要处理的消息的Java类型。在这个ChannelHandler中,你将需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext的引用,这个引用将作为输入参数传递给ChannelHandler的所有方法。
在这种类型的ChannelHandler中,最重要的方法是channelRead0(ChannelHandlerContext,T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。
引导
Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
因此,有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
类别 | Bootstrap | ServerBootstrap |
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |
这两种类型的引导类之间的第一个区别已经讨论过了:ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的。
第二个区别可能更加明显。引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个(也可以是同一个实例)。为什么呢?
因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接收的连接都有一个)的Channel。下图说明了这个模型,并且展示了为何需要两个不同的EventLoopGroup。
与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配一个EventLoop。