这三者可以看作是Netty网络架构的抽象
基本的IO操作(bind(),connect(),read(),write())都是基于底层的网络传输。在Java网络编程中,基础构建是Socket类,但是直接基于Socket进行编程会很繁琐。Netty的Channel接口提供的API能大大地减小直接对Socket进行编程的复杂性。另外,Channel又是很多实现了特定需求的Channel的顶层抽象接口,这些Channel接口的实现类有:
EventLoop用来处理一个连接(connection)生命周期内发生的事件。下图在高层次上描述了Channel、EventLoop、Thread和EventLoopGroup之间的关系。
这些关系有:
注意在这种设计中,给定Channel的IO操作都是在同一个线程中执行,无形中就不需要进行同步。
Netty中的所有IO操作都是异步的。因为一个操作的结果可能不会马上返回,所以我们需要一种能够在未来获取它的结果的方式。为此,Netty提供了ChannelFuture
,它的addListener()
方法注册了一个ChannelFutureListener
,通过这个ChannelFutureListener
就能在操作完成(不管有没有成功)时得到通知。
站在应用程序开发者的角度来看,Netty主要的组件就是ChannelHandler
,它作为所有为传入和传出数据提供处理的应用业务逻辑的容器。通常实现它(或继承实现了它的类)来提供业务逻辑。这是可能的因为ChannelHandler
的方法会被网络事件触发。实际上,ChannelHandler
能被用于任意类型的操作,比如,数据的格式转换或处理操作过程中抛出的异常。
你通常需要实现的是ChannelHandler
的子接口ChannelInboundHandler
。这个子接口能接收你业务逻辑需要处理的数据或者事件。当你要发送一个回复给客户端的时候,你能从ChannelInboundHandler
中flush数据。你的业务逻辑通过会依赖于多个ChannelInboundHandler
。
ChannelPipeline
为一连串的ChannelHandler
提供容器同时定义了用于传播链中传入数据和传出数据事件流的API。Channel
被创建后会自动的分配到它自己的ChannelPipeline
。
ChannelHandler
通过如下的步骤被加载到ChannelPipeline
中:
ChannelInitializer
的实现注册到ServerBootstrap
中ChannelInitializer.initChannel()
方法被调用,ChannelInitializer
装载了一系列自定义的ChannelHandlers
到pipeline中。ChannelInitializer
把它自己从ChannelPipeline
中移除掉 ChannelHandler
具有广泛的用途,你能把它想成一个为任何处理在ChannelPiple
中流通(传入传出)的事件(包括数据)的代码的通用容器(generic container)。下图说明了ChannelInboundHandler
和ChannelOutboundHandler
继承自ChannelHandler
的关系。
事件通过在初始化或启动应用期间加载的ChannelHandler
在pipeline中传递,ChannelHandler
接受事件并执行在其中实现了的业务逻辑,并将数据传递给(handler)链中的下一个handler。这些handler
根据它们被添加的顺序来执行。
上图展示了Netty应用中输入(数据)流和输出(数据)流的区别。对于客户端来说,从客户端到服务器事件就是传出去(outbound)的反之就是传进来(inbound)的。
inbound和outbound handler都能安装到同一个pipeline。如果读到一条消息或者其他传入(inbound)事件,它会从这条pipeline的头部(head)开始然后传递给第一个ChannelInboundHandler
。这个handler可能会也可能不会真的修改数据,这取决于它的具体函数,然后数据会传递给下一个handler。最终,数据回到达pipeline的末尾(tail),此时所有的处理都结束了。
数据传出(outbound)的移动(也就是数据被写)与传入同理。这种情况下,数据从尾部流到头部。
因为传出数据和传入数据是有区别的,你可能想知道当这两种不同的操作都混在了同一个ChannelPipeline
时会发生什么。虽然inbound和outbound handler都继承了ChannelHandler
,Netty提供了分别的实现(ChannelInboundHandler好ChannelOutbouondHandler)同时确保数据只会在同一个方向的handler中传递。
当一个ChannelHandler
被添加到ChannelPipeline
中,它会被分配一个代表ChannelHandler
和ChannelPipeline
之间绑定关系的一个ChannelHandlerContext
。尽管可以通过这个Context获取底层的Channel
实例,但是它主要用来写传出数据(write outbound data)。
在Netty中有两种发送数据的方式。你既可以直接写到Channel
里面又可以写到与ChannelHander
相关的ChannelHandlerContext
中。前者会使数据从ChannelPipeline
的末尾开始传输,后者会使数据从ChannelPipeline
中下一个handler开始。
正如我们前面说到的,Netty提供了不同类型的ChannelHandler
,它们的功能很大程度上是由它们的超类决定。Netty以适配器(adapter)类的方式提供了许多默认的handler实现以简化应用业务逻辑的开发。你已经知道在一个pipeline中每一个ChannelHandler
都负责把事件传递给下一个handler。这些适配器类(以及它们的子类)会自动这么做,因此你可以只重写你感兴趣的方法和事件。
接下来我们来了解3个ChannelHandler
的子类:encoders,decoders以及SimpleChannelInboundHandler(一个ChannelInboundHandlerAdapter的子类)。
当你通过Netty收发消息时,一个数据的转换就会发生。传入的消息会被解码(decode),从字节转换成其他的格式,通常是一个Java对象。如果是传出的消息,那么就会把数据从当前的格式转换为字节。因为网络数据总是字节流的。
Netty为编码解码提供了大量与特定需求相关的抽象类。比如,你的应该可能会使用一个中间(intermediate)格式,不需要将消息马上转换为字节。你仍然需要一个编码器(encoder),但它可从不同的超类中继承。为了决定使用哪一个,你可以应用一个简单的命名约定(apply a simple naming convention)。
通常,基类会有像ByteToMessageDecoder
或MessageToByteEncoder
的类名。在具体的实现类中,有与Protobuf相关的ProtobufEncoder
和ProtobufDecoder
。
所有的编码解码适配器类都实现了ChannelInboundHandler
或ChannelOutboundHandler
。
对于传入数据来说,你会发现channelRead
方法或事件被重写了。这个方法会被从传入数据(inbound)Channel读到的消息调用。然后会通过提供的decoder
调用decode()
方法然后将解码了的字节(解码成Java对象)传递给下一个ChannelInboundHandler
。
对于传出数据来说就是相反的,一个encoder
将消息转换成字节然后传递给一下handler。
通常你的应用需要使用一个handler来接收一个解码了的数据然后应用一些业务逻辑。为了创建这样只有个handler,你只需要继承SimpleChannelInboundHandler
,T
就是你想要处理的数据的Java类型。
这个类中最重要的方法是channelRead0(ChannelHandlerContext ctx,T t)
。它的实现完全取决于你,除非需要当前IO线程不被阻塞。
Nettyd的bootstrap
类为配置一个应用网络层的容器,这涉及到给一个特定的端口号绑定一个程序(process)(通常是用于启动服务器);或连接一个程序到另一个在特定的主机和端口号运行的程序(通常是用于启动一个客户端)。也就是监听连接请求或建立连接。
严格的说术语连接指的是面向连接协议如TCP,它可以保证传输数据的有序性。
因此,有两种bootstrap
:一种是为客户端设计的,就叫Bootstrap
;另一种是为服务端设计的(ServerBootstrap
)。
下表比较了这两种类的区别:
分类 | Bootstrap | ServerBootstrap |
---|---|---|
网络功能 | 连接到远程的主机和端口 | 绑定到一个本地端口 |
EventLoopGroup的数量 | 1 | 2 |
我们重点解释一下第二个不同,启动一个客户端只需要一个EventLoopGroup
,但服务端需要两个(可以使同一个实例)。为啥子呢?
一个服务器需要两个不同的Channel
集合。第一个集合包含一个ServerChannel
代表服务器自己绑定了本地端口的监听socket。第二个集合会包含所有被创建来处理客户端连接的Channel
。下图展示了这个模型。
与ServerChannel
相关的EventLoopGroup
(左边的)分配EventLoop
来处理为连接请求创建ServerChannel
,一旦一个连接被接收,第二个EventLoopGroup
就为这个Channel分配一个EventLoop
。