第三章 Netty 组件和设计
从高层视角,Netty address 两个等价的关注域:技术和架构
* 首先,它是构建于 Java NIO 上的异步、事件驱动的实现,保证了在高负载下最大的应用性能和伸缩性;
* Netty 体现了一组设计模式,从网络层解耦应用逻辑,简化开发,最大化可测试性、模块化、代码重用。
当我们更细微地学习 Netty 的独立组件时,我们将聚焦于它们是如何协作来支持这些架构最佳实践。通过遵循同样的原则,我们将获得 Netty 能提供的所有好处。
3.1 Channel, EventLoop, ChannelFuture
Channel, EventLoop, ChannelFuture 放到一起,可以代表了 Netty 的网络层抽象:
- Channel: Sockets
- EventLoop: 控制流,多线程,并发
- ChannelFuture: 异步通知
3.1.1 Channel 接口
基本的 I/O 操作(bind(), connect(), read(), write()) 依赖于底层网络传输的提供的原子操作。在基于 Java 的网络里,基础结构是 Socket
类。Netty 的 Channel 接口提供的 API 极大简化了直接操作 Sockets 的复杂工作。
3.1.2 EventLoop 接口
EventLoop 定义了 Netty 处理一个连接生命周期里发生的事件的核心抽象。Channel、EventLoop、EventLoopGroup 之间的关系如下:
- 一个 EventLoopGroup 包含一个或多个 EventLoop;
- 一个 EventLoop 在它的生命周期里是绑定到单一线程的;
- 一个 EventLoop 处理的所有 I/O 事件都是在它的专用线程上进行的;
- 一个 Channel 在它的生命周期里是注册到到单一的 EventLoop;
- 单一的 EventLoop 可能被赋给一个或多个 Channel。
注意,在这种设计里,给定 Channel 的 I/O 事件都由同一个线程来执行,事实上消除了对同步的需要。
3.1.3 ChannelFuture
接口
Netty 里的所有 I/O 操作都是异步的。因为一个操作可能没法立即返回,我们需要一种方法来稍后确定它的结果。处于这种目的,Netty 提供了 ChannelFuture
,它的 addListener()
方法可以注册 ChannelFutureListener
来在操作完成(不管是否成功)时获得通知。
可以把 ChannelFuture
看作是一个在未来执行的操作的结果占位符。它什么时候执行取决于多个因素,因此没法精确预测,但它肯定会被执行。更进一步,属于同一个 Channel
的所有操作保证按它们发起的顺序执行。
3.2 ChannelHandler 和 ChannelPipeline
这两个组件用于管理数据流动和执行应用程序的处理逻辑。
3.2.1 ChannelHanlder 接口
从应用开发者的角度,Netty 的首要组件是 ChannelHandler,它作为所有用于处理入站、出站数据的应用程序逻辑的容器。事实上,ChannelHandler 可用于处理几乎所有的操作,例如进行数据格式转换、处理异常。
例如,ChannelInboundHandler
接收入站事件和被应用程序业务逻辑处理的数据。当你要发送响应给客户端连接时,你也可以在 ChannelInboundHandler
上 flush 数据。你的应用的业务逻辑通常存在于一个或多个 ChannelInboundHandler
。
3.2.2 ChannelPipeline 接口
ChannelPipeline 提供了一个 ChannelHandler 链的容器,定义了一组 API 用于沿着这个链传递入站和出站事件。当一个 Channel 被创建后,它自动被赋予它自己的 ChannelPipeline 。
ChannelHandler 按如下方式安装到 ChannelPipeline :
- 一个 ChannelInitializer 实现被注册到 ServerBootstrap 。
ChannelInitializer.initChannel()
被调用,ChannelInitializer 安装一组 ChannelHandler 到 pipeline。- ChannelInitializer 从 ChannelPipeline 移除它自身。
ChannelHandler 特意设计为支持广泛的使用,你可以认为它是处理 进入、离开 ChannelPipeline 的事件(包括数据)的任意代码的容器。
在 pipeline 中移动事件是 ChannelHandlers 的工作。这些对象接收事件、执行它们实现的处理逻辑,传递数据给链条的下一个 handler 。它们执行的顺序取决于它们被添加的顺序。
图 3.3 显示了入站和出站处理器被安装到同一个 pipeline。如果一个消息或任意凄然入站事件被读取,它将从 pipeline 的头部开始,传递给第一个 ChannelInboundHandler 。这个处理器可能、也可能不实际修改数据,取决于它的具体功能,然后数据被传递给链条里的下一个 ChannelInboundHandler 。最终,数据将到达 pipeline 的尾部,在这里,所有的处理都结束了。
数据的出站运动在概念上是完全一样的。在这种情况下,数据从 ChannelOutboundHandler 链的尾部流过链条直到头部。超出这个点后,出站数据将到达网络传输,这里的是 Socket。典型地,这将触发一个写操作。
一个事件将向前转发给当前链条里的下一个 handler,通过使用 ChannelHandlerContext 对象。因为有时候需要忽略一些不敢兴趣的事件,Netty 提供了抽象基类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 。每个类提供的方法都简单地用 ChannelHandlerContext 对象简单地把事件传递给链条中的下一个 handler。
给定的出站和入站操作是不同的。虽然入站和出站的 handler 都扩展自 ChannelHandler,Netty 区分 ChannelInboundHandler 和 ChannelOutboundHandler 的实现,保证数据只在同一方向的 handler 类型之间传递。
当 ChannelHandler 被添加到 ChannelPipeline ,它被赋予一个 ChannelHandlerContext,表示 ChannelHandler 和 ChannelPipeline 之间的绑定。虽然这个对象可用于获取底层的 Channel,它通常用于写出站数据。
在 Netty 发送消息的两种方式:
* 直接写入 Channel :从 ChannelPipeline 的尾部开始(也就是所有 Handler 都有机会进行处理);
* 写入 ChannelHandlerContext :消息从 ChannelPipeline 下一个处理器开始;
3.2.3 近看 ChannelHandler
有很多种类型的 ChannelHandler,每个的功能大多由它的父类决定。Netty 以适配器类的形式提供了很多默认处理器的实现,用于简化应用程序处理逻辑的开发。pipeline 里的每个 ChannelHandler 负责把事件正向传递到链里的下一个处理器。适配器类和它的子类自动完成这些,你只需要覆写希望定制化传递方法和事件。
最常用的的适配器:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
- ChannelDuplexHandlerAdapter
3.2.4 Encoder 和 Decoder
当你用 Netty 发送或接收消息时,数据转换发生了。入站消息将被 解码:从字节转换到另一个格式,通常是 Java 对象。如果消息出站,将发生发转:消息将从当前格式编码为字节。这两种转换的理由很简单:网络数据总是一序列的字节。
各种不同的抽象类被提供用于编码和解码,对应于具体的需要。例如,应用程序可能用一种中间格式,不要求消息立即转换到字节。你仍然需要一个编码器,它将继承自不同的父类。
严格地讲,其他的处理器也可以做编码器和解码器的工作。但正如存在那些适配器用于简化 channel 处理器的创建,Netty 提供的所有编码器/解码器要么是 ChannelInboundHandler 要么是 ChannelOutboundHandler 的实现。
你将发现,对于入站数据, channelRead
方法/事件被覆写。对于从入站管道读取的每个消息,这个方法将被调用。然后调用解码器提供的 decode()
方法,然后把解码得到的字节传递给 pipeline 里的下一个 ChannelInboundHandler 。
对于出站消息的模式是相反的:一个编码器把消息转换为字节,正向传递给下一个 ChannelOutboundHandler 。
3.2.5 SimpleChannelInboundHandler 抽象类
通常,你的应用程序采用一个处理器来接收解码后的消息,对数据应用业务逻辑。为创建这样一个 ChannelHandler ,你只需要继承自基类 SimpleChannelInboundHandler<T>
,T
是你想处理的 Java 消息类型。
这种类型的处理器最重要的方法
3.3 Boostrapping
Netty 的 bootstrap 类提供了应用的网络层配置的容器,它要么绑定一个进程到给定端口,要么连接一个进程到另一个运行在指定主机和端口的进程。
“服务器”是监听一个或多个入站的进程,用 ServerBootstrap
;“客户端”是建立连接,用 Bootstrap
。
面向连接的协议:“连接”这个术语只应用于面向连接的协议,例如 TCP,它保证连接两端传递的消息的顺序。
Category | Bootstrap | ServerBootstrap |
---|---|---|
网络功能 | 连接到一个远程主机和端口 | 绑定到本地端口 |
EventLoopGroup 数量 | 1 | 2 |
一个服务器需要区分两组 Channel
。第一组包含单一的 ServerChannel
,表示服务器自身正在监听的套接字,绑定到一个本地端口。第二组包含所有已经创建的用于处理到来的客户端连接的 Channel
— 每个服务器已接受的连接对应一个 channel。
3.4 总结
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。