从高层次的角度来看,Netty解决了两个相应的关注领域,我们可将其大致标记为技术的和体系结构的。
首先,它的基于Java NIO的异步的和事件驱动的实现,保证了高负载下应用程序性能的最大化和可伸缩性。其次,Netty也包含了一组设计模式,将应用程序逻辑从网络层解耦,简化了开发过程,同时也最大限度地提高了可测试性、模块化以及代码的可重用性。
Channel——Socket;
EventLoop——控制流、多线程处理、并发;
ChannelFuture——异步通知。
基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的源语。在基于Java的网络编程中,其基本的构造是class Socket。Netty的Channel接口所提供的API,大大地降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
EmbeddedChannel;
LocalServerChannel;
NioDatagramChannel;
NioSocketChannel.
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的时间。下图在高层次上说明了Channel、EventLoop、Thread以及EventLoopGroup之间的关系。
这些关系是:
(1)一个EventLoopGroup包含一个或者多个EventGroup;
(2)一个EventLoop在它的生命周期内只和一个Thread绑定;
(3)所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理;
(4) 一个Channel在它的生命周期内之只注册一个EventLoop;
(5)一个EventLoop可能会被分配给一个或多个Channel。
(注意:在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要。)
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
(关于ChannelFuture的更多讨论:可以将ChannelFuture看作是讲来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。此外,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行。)
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为ChannelHandler的方法时由网络事件触发的。事实上,ChannelHandler可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另一种格式,或者处理转换过程中所抛出的异常。
举例来说,ChannelInboundHandler是一个你将会经常实现的子接口。这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的应用程序的业务逻辑通常驻留在一个或者多个ChannelInboundHandler中。
ChannelPipeline接口ChannelPipeline为ChannelHandler链提供了容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,他会被自动地分配到它专属的ChannelPipeline。
ChannelHandler安装到ChannelPipeline中的过程如下所示:
(1)一个ChannelInitializer的实现被注册到了ServerBootstrap中;
(2)当ChannelPipeline.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline中安装一组自定义的ChannelHandler;
(3)ChannelInitializer将它自己从ChannelPipeline中移除。
入站和出站CHannelHandler可以被安装到同一个ChannelPipein中。如果一个消息或者其他入站消息被读取,那么它会从ChannelPipeline的头部开始流动,并被传递给第一个ChannelInboundHandler,这个ChannelHandler不一定会实际地修改数据,具体取决于她的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达ChannelPipeline的尾端,届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。
关于入站和出站ChannelHandler的更多讨论
通过使用作为参数传递到每个地方的ChannelHandlerContext,事件可以被传递给当前ChannelHandler链中的下一个ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。通过调用ChannelHandlerContext上的对应方法,每个都提供了简单地将事件传递给下一个ChannelHandler的方法的实现。随后,你可以 通过重写你所感兴趣的方法来扩展这些类。
虽然ChannelInboundHandle和ChannelOutboundHandle都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递。
当ChannelHandler被添加到ChannelPipeline时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式。你可以直接写到Channel中,也可以写到和ChannelHandler相关联的ChannelHandlerContext对象中。前一种方式将会导致消息从ChannelPipeline的尾端开始流动,而后者将导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。
正如我们之前所说的,有许多不同类型的ChannelHandler,它们各自的功能主要取决于它们的超类。Netty以适配器类的形式提供了大量默认的ChannelHandler实现,其旨在简化应用程序处理逻辑的开发过程。ChannelPipeline中的每个ChannelHandler将会负责把事件转发到链中的下一个ChannelHandler。这些适配器类(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。
为什么需要适配器类
有一些适配器类可以将编写自定义的ChannelHandler所需要的努力降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。
下面这些是编写自定义ChannelHandler时经常会用到的适配器类:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
ChannelDuplexHandler
当你通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
对应于特定的需要,Netty为编码器和解码器提供了不同类型的抽象类。例如,你的应用程序可能使用了一种中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是它将派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单的命名约定。
通常来说,这些基类的名称将类似于ByteToMessageDecoder或MessageToByteEncoder。对于特殊的类型,你可能会发现类似于ProtobufEncoder和ProtobufDecoder这样的名称——预置的用来支持Google的Protocol Buffers。
严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化ChannelHandler的创建的适配器一样,所有由Netty提供的编码器/解码器适配器类都实现了ChannelOutHandler或者ChannelInboundHandler接口。
你将会发现对于入站数据来说,channelReaf方法/事件已经被重写了。对于每个从入站Channel读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器提供的decode()方法,并将已解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
出站消息的模式是相反方向的:编码器将消息转换为字节,并将它们发送给下一个ChannelOutboundHandler。
最常见的情况是,你的应用程序会利用一个ChannelHandler来接收解码消息,并对该数据应用业务逻辑。要创建一个这样的ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler
在这种类型的ChannelHandler中,最重要的方法是channelRead0(ChannelHandlerContext, T)。除了要求不要阻塞当前的I/O线程之外,其具体实现完全取决于你。
Netty的引导类为应用程序的网络层配置提供了容器,这设计将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程。
引导有两种类型:一种用于客户端(Bootstrap),而另一种用于服务器(ServerBootstrap)。
类别 | Bootstrap | ServerBootstrap |
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |
为什么引导一个客户端只需要一个EventLoopGroup,但是引导一个服务端则需要两个(也可以是同一个实例)?
因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。