本文是Netty文集中“Netty in action”系列的文章。主要是对Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一书简要翻译,同时对重要点加上一些自己补充和扩展。
本章涵盖
- Netty的技术和结构方面
- Channel、EventLoop和ChannelFuture
- ChannelHandler和ChannelPipeline
- 引导
Channel,EventLoop,and ChannelFuture
下面我们将增加对Channel、EventLoop和ChannelFuture类的讨论,这些类一起代表了Netty网络的抽象
- Channel —— Sockets
- EventLoop —— 控制流,多线程和并发
- ChannelFuture —— 异步的通知
Channel接口
基本的I/O操作( bind(),connect(),read(),and write() )依赖于原生底层网络传输的支持。在基于Java的网络中,基本的结构是Socket类。Netty的Channel接口提供了一个API,这更好的减少了直接使用Sockets工作的复杂性。此外,Channel是扩展类系统中的根,拥有许多的预定义实现,比如:
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
Interface EventLoop
EventLoop定义了“Netty处理一个连接的生命周期中遇到的所有事件”的核心抽象概念。
Channels,EventLoops,Threads 和 EventLoopGroups 的关系:
- 一个EventLoopGroup包含一个或多个EventLoop
- 一个EventLoop被绑定到一个单线程上,在这个EventLoop的整个生命周期。
- 所有的I/O事件处理通过一个EventLoop在一个专门的线程上被处理。
- 一个Channel的整个生命周期里只会被注册到一个EventLoop
- 一个EventLoop可能被分配给一个或多个Channel
ChannelFuture接口
正如我们所解释的,Netty中的所以I/O操作都是异步的。因为操作可能立即返回,之后我们需要一个方式去检测这个操作的返回。为了这个目的,Netty提供了ChannelFuture,ChannelFuture的addListener()方法可以注册一个ChannelFutureListener,该ChannelFutureListener在操作完成( 无论操作成功与否 )时将被通知。
更多关于ChannelFuture
想象ChannelFuture作为一个占位符用于一个操作的结果,它将在未来某个时刻被执行。什么时候会被执行可能依赖几个因素,这可能无法精确的预测,但是能保证的是它在未来某个时刻一定会被执行。此外,所有属于同一个Channel的操作将保证按照调用的顺序被执行。
ChannelHandler 和 ChannelPipeline
ChannelHandler接口
从应用开发者的观点来看,Netty最主要的组件就是ChannelHandler,ChannelHandler作为所有应用逻辑的容器,用于处理出站和入站的数据。这可能是因为ChannelHandler的方法会被网络事件触发。事实上,一个ChannelHandler能致力于几乎所有的动作类型,比如将数据格式从一种转换到另一种或者处理执行过程中抛出的异常。
举个例子,ChannelInboundHandler是一个子接口,你将频繁实现这个接口。这种类型接受入站事件和数据,你的应用逻辑会对其进行处理。你还能在一个ChannelInboundHandler里刷新数据(flush data)当你要发送一个响应到一个连接的客户端时。我们的应用逻辑将经常属于一个或多个ChannelInboundHandlers。
ChannelPipeline接口
一个ChannelPipeline提供了一个ChannelHandlers 链的容器,并且定义了API用于在ChannelHandlers链中传播入站流和出站事件。
当一个Channel被创建的时候,它将自动分配它自己的ChannelPipeline。
ChannelHandlers被装进ChannelPipeline遵循如下步骤:
- 一个ChannelInitializer实现被注册到一个ServerBootstrap
- 当ChannelInitializer.initChannel()被调用,ChannelInitializer将一个自定义的ChannelHandlers集合安装至管道中。
- 将ChannelInitializer自己从ChannelPipeline中移除。
让我们更深入ChannelPipeline和ChannelHandler的生态关系以观察当你发送或接受数据时都发生了什么事。
ChannelHandlers接收事件,执行已经实现的处理逻辑,并传递处理后的数据到链中的下一个处理器(ChannelHandler)。ChannelHandler执行的顺序取决于它们被加入到链中的顺序。
入站和出站处理器能被放入到同一个管道中。如果一个消息或者任何其他的进站事件被读取,它将从管道的头开始传递给第一个ChannelInboundHandler。这个处理器可能会也可能不会真实的修改数据,这依赖于特定的功能,接下来数据会被传递到链中的下一个ChannelInboundHandler,最后数据将到达pipeline的尾部,到此为止入站数据的所有处理结束。
数据出站和入站是类似的,出站数据从ChannelPIpeline的尾部的第一个ChannelOutboundHandler开始,直到数据到达pipeline头。越过这个点,出站数据将到达网络传输,这里显示为Socket。最经典的,socket将触发一个写操作。
更多关于入站和出站处理器
通过ChannelHandlerContext能将一个event传递到chain中的下一个handler,该ChannelHandlerContext在作为一个参数支持于每个方法中( 即,每个方法都有ChannelHandlerContext这个参数 )。
因为在某些时候你想忽略你所不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。这两个抽象基类简单实现了所以的方法:通过调用ChannelHandlerContext对应的方法将事件传递给下一个handler。你能继承这类并重写你所感兴趣的方法。
考虑出站和入站操作的不同,你可能会担心当两个类型的处理器混合在一个ChannelPipeline中会发生什么。虽然,入站和出站处理器都继承了ChannelHandler,但Netty区分了ChannelInboundHandler和ChannelOutboundHandler的实现并确保数据只会在两个相同方向类型的处理器间传递。
当一个ChannelHandler被加到一个ChannelPipeline中,它分配了一个ChannelHandlerContext,该ChannelHandlerContext代表了一个ChannelHandler和ChannelPipeline的绑定。虽然ChannelHandlerContext对象能被用于获取底层的Channel,但大多时候可以直接利用ChannelHandlerContext去写一个出站数据。
Netty两种发送消息的方法:
①通过Channel来发送,如:ChannelHandlerContext.channel.writer(obj)
通过Channel发送的数据会从ChannelPipeline尾部开始传递到ChannelPipeline的头部,接着进行数据的网络传输
②通过在与之关联的ChannelHandler中的ChannelHandlerContext写数据,如:ChannelHandlerContext.writer(obj)
而通过某个ChannelHandler关联的ChannelHandlerContext进行数据发送时,数据将从当前ChannelHandler对应的下一个ChannelHandler开始执行,即写入的数据会直接传递给链中的下一个ChannelOutboundHandler。
进一步看ChannelHandlers
正如我们前面所说的,ChannelHandlers有许多不同类型的,并且它们的功能很大程度上取决于它们的父类。Netty提供了许多默认处理器的实现以适配器类的形式,这么做的目的在于简化应用程序的开发。
你已经看到pipeline中的每一个ChannelHandler负责传递事件到链中的下一个handler(这个传递工作实际上是由ChannelHandlerContext完成的)。这些适配器(或其子类)将自动帮我们完成。
为什么使用适配器
这些适配器最大程度上的帮助我们减小了自定义ChannelHandler的工作量,因为他们提供了对应接口所有方法的默认实现。
下面这些适配器在你创建自定义的处理器时会经常使用到:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandlerAdapter
编码器和解码器
编码和解码:当你在Netty发送或者接收一个消息时,一个数据进行转换的地方。
一个入站消息将被解码,这是将字节转换为另一个数据格式,典型的例子是转换为一个java对象。如果是出站消息,这将是相反的:当前数据格式将编码成字节。这两个转换的原因是因为:网络数据总是一系列字节。
Netty提供了多种类型的编码和解码抽象类,对应于具体的需求。还提供了将消息转换成另一种中间格式,而不立即转换成字节,这样的编码器需要不同的父类来派生。
基本的编码器、解码器,如:MessageToByteEncoder、ByteToMessageDecoder
专业的类型的编码器、解码器,如:ProtobufEncoder、ProtobufDecoder
所有Netty提供的encoder/decoder 适配器类要么实现了ChannelInboundHandler,要么实现了ChannelOutboundHandler
解码器重写了channelRead方法,channelRead方法中调用了解码器提供的decode方法,并将解码后的数据传递给了pipline中的下一个ChannelInboundHandler。编码器则与之相反。
SimpleChannelInboundHandler抽象类
在大多时候你的应用将引用一个handler用于接收解码后的数据,并对该数据进行商业逻辑处理。想创建这样的一个ChannelHandler,你只需要继承基类SimpleChannelInboundHandler<T>, T 是你想要处理的消息的java类型。在这个处理器中,你将重写一个或多个基类中的方法并获取一个ChannelHandlerContext引用,该ChannelHandlerContext引用会作为一个参数在所有的处理器方法中。
channelRead0(ChannelHandlerContext, T)是一个非常重要的方法在SimpleChannelInboundHandler中。除了要求当前I/O线程不被堵塞外,这个方法实现完全取决于你。
Bootstrapping
Netty的启动引导类提供了用于应用网络层配置的容器,包括绑定程序到一个给定端口或一个程序通过指定的host、port连接到另一个程序。
一般而言,我们将前一种情况称为引导一个服务端,后一种情况为引导一个客户端。这个术语简单又方便,但是轻微模糊了“服务端”和“客户端”表示不同网络行为的重要事实。也就是,‘监听进来的连接’与‘和一个或多个进程建立连接’。
Bootstrap 和 ServerBootstrap 的区别
①一个ServerBootstrap绑定一个端口,因为服务端必须监听连接。而Bootstrap用于想要连接远端的客户端应用。
②引导一个客户端和需要一个EventLoopGroup,而服务端(ServerBootstrap)需要两个EventLoopGroup( 这两个可以是同一个实例 )。
一个服务端需要两个不同的Channel集合。第一个集合包含了ServerChannel,该ServerChannel代表服务自己所监听的绑定本地端口的socket。第二个集合将包含所有已经创建了的Channel,这些Channel ( 该Channel由ServerChannel创建 )用于处理客户端连接,服务端收到的每一个客户端的连接都将创建一个Channel。
ServerChannel所关联的EventLoopGroup会分配一个EventLoop用于负责在收到连接请求时创建Channel。一旦接收一个连接,第二个EventLoopGroup就会分配一个EventLoop给创建好的Channel。
注意,这里Channel的创建是由ServerChannel所在的EventLoop( 实际上是EventLoop所在的线程上 )完成的。而且一个ServerChannel只会注册到一个EventLoop上。
后记
本文主要对Netty主要的组件进行了介绍,同时介绍了Netty框架的一些设计思想。是一篇很浅的概述介绍文章,其中涉及的组件都会在其他章节进行详细展开以及深入的学习。
若文章有任何错误,望大家不吝指教:)
网友评论