这里将回顾我们之前章节讲到过的主要概念和组件。
1 Channel 、EventLoop和ChannelFuture
- Channel —— Socket;
- EventLoop —— 控制流、多线程处理、并发;
- ChannelFuture —— 异步通知。
1.1 Channel 接口
基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在基于Java的网络编程中,其基本的构造是Class Socket。Netty的Channel接口所提供的API,大大降低了直接使用Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSockectChannel
1.2 EventLoop接口
EventLoop定义了Netty的核心抽象,用于处理连接的声明周期中所发生的事件。下图在高层次上说明Channel、EventLoop、Thread以及EventLoopGroup之间的关系。
- 一个EventLoopGroup包含一个或者多个EventLoop
- 一个EventLoop在它的生命周期内只和一个Thread绑定
- 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理
- 一个Channel在它的生命周期内只注册于一个EventLoop
- 一个EventLoop可能会被分配给一个或多个Channel
注意,在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的,实际上消除了对于同步的需要。
1.3 ChannelFuture接口
Netty中所有的I/O操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便于在某个操作完成时(无论是否成功)得到通知。
可以将ChannelFuture看作将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。此外,所有属于同一个Channel的操作都被保证其将它们被调用的顺序执行。
2 ChannelHandler 和 channelPipeline
我们将更加细致地看一看那些管理数据流以及执行应用程序处理逻辑的组件。
2.1 ChannelHandler接口
从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。实际上,ChannelHandler可专门用于几乎任何类型的动作,如将数据从一种格式转换为另一种格式,或处理转换过程中排除的异常。
其中,ChannelInboundHandler是开发人员经常实现的子接口,可以接收入站事件和数据。这些数据在随后将会被应用程序的业务逻辑所处理。
2.2 ChannelPipeline接口
ChannelPipeline提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出站事件流的API。当Channel被创建时,它会被自动地分配到它专属的ChannelPipeline。
ChannelHandler安装到ChannelPipeline中的过程如下所示:
- 一个ChannelInitalizer的实现被注册到了ServerBootstrap中;
- 当ChannelInitalizer.initChanel()方法被调用时,ChannelInitalizer将在ChannelPipeline中安装一组自定义的ChannelHandler;
- ChannelInitializer将它自己从ChannelPipeline中移除
ChannelHandler是专为支持广泛的用途而设计的,可以将它看作处理往来ChannelPipeline事件(包括数据)的任何代码的通用容器。类图如下所示:
ChannelPipeline是ChannelHandler的编排顺序。下图将说明Netty应用程序中入站和出站数据流之间的区别。从客户端角度看,如果事件的运动方向是从客户端到服务端,那么我们称这些事件为出站,反之则为入站。
上图显示了入站和出站的ChannelHandler可以被安装到同一个ChannelPipeline中。如果一个小希或者任何其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,并被传递给第一个ChannelInboundHandler。这个ChannelHandler不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达ChannelPipeline的尾端。届时,所有处理就都结束了。
数据的出站运动(即正在被写的数据)的概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部位置。在这之后,出站数据将会到达网络传输层,这里显示为Socket。通常情况下,这将触发一个写操作。
关于入站和出站channelHandler的更多讨论
通过使用作为参数传递到每一个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler链中的下一代ChannelHandler。因为你有时候会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutHandlerAdapter。通过调用ChannelHandlerContext上的对应方法,每个都提供了简单地将事件传递给下一个ChannelHandler的方法实现。随后,你可以通过重写你感兴趣的那些方法来扩展这些类。
鉴于出站操作和入站操作是不同的,如果两个类别的ChannelHandler都混合添加到同一个ChannelPipeline中会发生什么?虽然ChannelInboundHandler和ChannelOutboundHandler都扩展自ChannelHandler,但是Netty能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递。
当ChannelHandler被添加到ChannelPipeline时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler和ChannelPipeline之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。
在Netty中,有两种发送消息的方式。
- 直接写到Channel中,导致信息从ChannelPipeline的尾端开始流动。
- 写到和ChannelHandler相关联的ChannelHandlerContext对象中,导致消息从ChannelPipeline中的下一个ChannelHandler开始流动。
2.3 更加深入地了解ChannelHandler
ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个ChannelHandler.这些适配器类(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。
为什么需要适配器类
有一些适配器类可以编写自定义的ChannelHandler所需要的努力降到最低限度,因为他们提供了定义在对应接口中的所有方法的默认实现。
下面这些是编写自定义ChannelHandler时经常会用到的适配器类:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandler
我们将研究3个ChannelHandler的子类型:编码器、解码器和SimpleChannelInboundHandler<T>-ChannelInboundHandlerAdapter的一个子类。
2.4 编码器和解码器
当程序员通过Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码。也就是说,从字节转换为另一种格式,通常是一个Java对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。
这两种方向的转换原因,即网络数据总是一系列的字节。
对应于特定的需求, Netty为编码器和解码器提供了不同类型的抽象类。如,你的应用程序可能使用了一个中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是它将派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单的命名约定。
通常来说,这些基类的名称类似于ByteToMessageDecoder或MessageToByteEncoder。对于特殊的类型,你可能会发现类似于ProtobufEncoder和ProtobufDecoder这样的名称——预置的用来支持Google的Protocol Buffers。
严格说来,其他的处理器也能够完成编码器和解码器的功能,即Netty提供的编码器/解码器适配器类都实现了ChannelOutboundHandler或ChannelInboundHandler接口。
2.5 抽象类SimpleChannelInboundHandler
当我们要创建一个ChannelHandler来接收解码消息时,可以扩展基类SimpleChannelInboundHandler<T>,其中T是要处理的消息的Java类型。在这个ChannelHandler中将需要重写基类的一个或者多个方法,并且获取一个到ChannelHandlerContext的引用,这个引用将作为输入参数传递给ChannelHandler的所有方法。
基于SimpleChannelInboundHandler实现的Handler,最重要的方法是 channelRead0(ChannelHandlerContext,T)。除了要求不阻塞当前的I/O线程之外,其具体实现完全取决于开发者。
3 引导
Netty的引导类为应用程序的网络层配置提供了容器,这涉及将一个进程绑定到某个指定的端口(引导一个服务器),或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程(引导一个客户端)。
面向连接的协议:严格来说,“连接”这个术语仅适用于面向连接的协议,如TCP,其保证了两个连接端点之间消息的有序传递。
因此有两种类型的引导,Bootstrap可用于引导客户端,ServerBootstrap可用于引导服务器。如下所示,比较了这两种类型的引导类。
类别 | Bootstrap | ServerBootstrap |
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数目 | 1 | 2 |
ServerBootstrap绑定到了一个端口,因此服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用。引导一个客户端只需要一个EventLoopGroup,但ServerBootStrap则需要两个。
为啥服务端需要两个EventLoopGroup?
是因为服务端需要两组不同的channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接的channel。以下图将说明服务端为啥需要两个不同的EventLoopGroup。
4 总结
这里我们从技术和体系结构两个角度探讨了Netty的重要性,也更加详细地介绍了一些概念和组件,特别是对ChannelHandler、ChannelPipeline和引导等概念做了阐述。
另外,我们讨论了ChannelHandler类的层次结构,对编码器/解码器做了介绍,并描述了他们在数据和网络字节格式之间来回转换的互补功能。
后面的文章将更深入介绍这些组件,并剖析原理。