主要内容
- Channel、EventLoop 和 ChannelFuture
- ChannelHandler 和 ChannelPipeLine
- 引导(ServerBootStrap 和 BootStrap
3.1 Channel、EventLoop 和 ChannelFuture
其实。这三个抽象接口组合在一起可以认为是 Netty 网络抽象的代表:
Channel——Socket
EventLoop——用于控制流、多线程处理和并发等等
ChannelFuture——异步通知
3.1.1 Channel
基本的 IO 操作(bind()、connect()、read()以及 write())依赖于底层的网络传输所提供的原语。它是基于 java 中的 Socket 而创建的。但是在 Netty 中 Channel 大大简化了直接使用 Socket 的复杂性。此外,Channel 还拥有许多预定义的、专门化实现的广泛类层次结构的根,如下
EnbeddedChannel
LocalServerChannel
NioDatagramChannel
NioSctpChannel
NioSocketChannel
3.1.2 EventLoop
是 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。下图在高抽象层次上说明了 Channel、EventLoop、Thread、和EventLoopGroup之间的关系
关系:
一个EventLoopGroup 包含一个活着多个 EventLoop
一个 EventLoop 在它的生命周期内只能和一个 Thread 绑定
一个 Channel 在它的生命周期内只注册于一个 EventLoop
一个 EventLoop 可能会被分配给一个或者多个 Channel
所以,由以上关系可以可知,一个给定的 Channel 的 IO 操作都是由相同的 Thread 执行的,因而不会出现同步的问题
3.1.3 ChanneFuture
由于 Netty 中所有的操作都是异步的。又因为一个操作可能不会立刻返回结果,比如读取文件,所以需要一种用于在之后确定其结果的方法。因此设计了 ChannelFuture 接口,其 addListener()方法注册一个 ChannelFutureListener,以便在某个操作完成后(成功或者失败)得到通知。
3.2 ChannelHandler 和 ChannelPipeLine
3.2.1 ChannelHandler
对于开发人员来说,ChannelHandler 是处理所有入站和出站数据的业务逻辑的容器。这些业务逻辑一般包括:数据格式的转换、数据处理异常的抛出等等。可以根据业务逻辑场景的不同而创建不同的 ChannelHandler 去处理,扩展性非常强大且不会影响系统的稳定性
3.2.2 ChannelPipeline
前面说到可以同时创建多个 ChannelHandler 来处理不同的业务场景,那么问题就来了:
- 怎么管理多个 ChannelHandler?
- 怎么让 ChannelHandler 的业务逻辑按照指定的顺序执行?
为了解决上面的问题,于是就有了 ChannelPipeLine 。ChannelPipeLine 的作用就是为多个 ChannelHandler 提供一个容器,同时把 ChannelHandler 按照指定的顺序串联起来(就是 ChannelHandler 在添加时候的顺序),形成一个 ChannelHandler 链。ChannelHandler 链形成的大致过程:
- 在引导(ServerBootStrap或者BootStrap)中注册一个 ChannelInitializer 实例
- 当 ChannelIniatializer.initChannel()方法被调用时,ChannelInitializer 将在 ChannelPipeLine 中添加一个自定义的 ChannelHandler
- ChannelInitialzer 主动从 ChannelPipeLine 中移除
为了进一步探究 ChannelPipeLine 和 ChannelHandler 之间的关系,接下来我们以客户端应用程序的角度来看它们之间的关系(出站:数据从本地传输到远程, 进站:数据从远程传输到本地)如下图
从上图可以看出来,一个 ChannelPipeLine 中可以同时包含入站和出站的 ChannelHandler。对于入站事件,它将会从 ChannelInboundHandler链的头部开始流动,并且传递到下一个ChannelInboundHandler,直到 ChannelPipeLine 的尾部。对于出站事件,它将会从 ChannelOutboundHandler 链的尾部开始流动,一直到链的头部为止。在这之后,数据会到达网络的传输层(这里显示为 Socket)。
这里有一个问题,数据是怎么在 ChannelHandler 链中进行传递的?
为了解决这个问题,Netty 中定义一个 ChannelHandlerContext 的接口,它作为一个参数传递到每个方法,使得事件通过它就可以在 ChannelHandler 链中进行传递,这是因为 ChannelHandlerContext 中的对应的方法,每一个都提供简单地将事件传递给下一个 ChannelHandler 的实现。而我们在写数据的时候,都会用到 ChannelHandlerContext 中的方法。
ChannelHandlerContext 代表了 ChannelHandler 和 ChannelPipeLine 之间的绑定,每当 ChannelHandler 被添加 ChannelPipeLine 时,它将会被分配一个 ChannelHandlerContext。
Netty 中发送消息有两种方式:直接写到 Channel 中,也可以写到 ChannelHandlerContext 中。两种方式的区别在于,第一种会使得消息从 ChannelPipeLine 的尾部开始流动,而第二种方式会使得消息直接从下一个 ChannelHandler 开始流动,消耗要少一点。
3.2.3 一些很重要的 ChannelHandler 子类
编码器和解码器
用于网络中的数据传输都只能是字节,所以对于出站的数据(一些java对象:String,pojo等等),需要通过编码器进行编码,转换为字节,在网络上进行传输。对于入站的数据,则要经过相反的操作,将字节转换为 java 对象,这个过程称为解码。
在 Netty 中定义一些和编解码相关的基类,类似于 ByteToMessageDecoder 或者 MessageToByteEncoder。对于一些特殊的类型,例如 ProtobufEncoder 和 ProtobufDecoder 这样的名称,用于支持 Google 的 Protocol Buffers。
SimpChannelInboundHandler<T>
该类使用于这样的情况:只需要接收到解码后的消息来进行业务逻辑,而没有其它很复杂的操作。泛型 T 代表了将要处理的 java 类型。该类中最重要的方法是 messageReceived()——用于接收消息,在 Netty5.0 方法名为 channelRead0()。
3.3 引导
Netty 的引导类为网络层的配置提供了容器,这涉及到将一个进程绑定到某个指定的端口,或者将一个进程连接到一个正在运行在某个指定主机的指定端口的进程。前一个称为引导一个服务器,后一个称为引导一个客户端。
所以,Netty 提供了两种引导类型:BootStrap——引导一个客户端,ServerBootStrap——引导一个服务器。
这两种引导有什么区别了?看下图
类别 | BootStrap | ServerBootStrap |
---|---|---|
网络编程中的作用 | 连接到远程主机 | 绑定到一个本地端口 |
EventLoopGroup的数量 | 1 | 2 |
- | - | - |
ServerBootStrap 为什么需要两个 EventLoopGroup?
因为服务器端的负载会远远大于客户端,一个 EventLoopGroup 既用来创建与客户端的连接,又用来处理各种各样的业务逻辑,这肯定是处理不过来的。所以服务器端需要两个 EventLoopGroup,其中一个只用于与客户端创建连接,另一个只用处理业务逻辑。如下图
与 ServerChannel 相关联的 EventLoopGroup 将会分配一个 EventLoop ,用于为连接请求创建 Channel。如果连接被接受了,那么,第二个 EventLoopGroup 就会给已经创建的 Channel 分配一个 EventLoop,用于处理流,业务逻辑等等。
问题
ChannelHandlerContext 传递消息的实现细节?
ChannelPipeLine 自动区别入站相关 Channel 和 出站相关Channel,并形成对应的入站 Handler 链 以及 出站 Handler 链的实现细节?