点击查看 《Netty in Action》笔记目录。
本文是对《Netty in Action》第4章内容的笔记和翻译,主要内容包括:
- OIO:阻塞传输
- NIO:异步传输
- 本地传输:和 JVM 异步交互
- 测试你的
ChannelHandler
案例学习:传输迁移
不通过 Netty 使用 OIO 和 NIO
我们将要展示基于 JDK API 的阻塞(OIO)和异步(NIO)应用版本。下面展示了阻塞实现版本。
下面展示了非阻塞版本。
通过 Netty 使用 OIO 和 NIO
非阻塞 Netty 版本
因为 Netty 对每一种传输实现都暴露了相同的 API,所以无论你采用哪一种传输,你的代码几乎不用修改。
传输 API
传输 API 的核心是 Channel
接口,所有的 I/O 操作都会使用这个接口。Channel
类的继承关系如图4.1所示。
Channel
中组合了 ChannelPipeline
和 ChannelConfig
。ChannelConfig
保存了 Channel
所有的配置,并且支持热更新。
由于 Channel
是独一无二的,所以 Channel
继承了 java.lang.Comparable
接口,从而保证对象的有序。因此,如果两个 Channel
实例返回相同的 hash 值,那 AbstractChannel
中 compareTo()
的实现将会抛出 Error
。
ChannelHandler
常用于:
- 转换数据格式。
- 提供异常通知。
- 提供
Channel
被激活或者关闭的通知。 - 提供
Channel
从EventLoop
中注册和取消注册的通知。 - 提供用户定义事件的通知。
拦截过滤器
ChannelPipeline
实现了一个常用设计模式:拦截过滤器。UNIX 管道是这个模式的另一个典型例子:命令被链式编排,每个命令的输出会作为接下来一个命令的输入。
你同样可以在运行时按照你的需求为 ChannelPipeline
添加或删除 ChannelHandler
。
除了可以使用 ChannelPipeline
和 ChannelConfig
之外,你还可以使用 Channel
的方法,下表展示了一些常用的方法。
方法名称 | 描述 |
---|---|
eventLoop |
返回与 Channel 绑定的 EventLoop 。 |
pipeline |
返回与 Channel 绑定的 ChannelPipeline 。 |
isActive |
如果 Channel 是激活(active)的,返回 true 。 Active 的含义取决于下层的传输协议。例如, Socket 中的 active 是指与远端建立连接;Datagram 中的 active 是指 Datagram 打开。 |
localAddress |
返回本地的 SocketAddress . |
remoteAddress |
返回远端的 SocketAddress . |
write |
将数据写到远端。数据会经过 ChannelPipeline ,并将会进行排队,直到刷新时才会发送。 |
flush |
将之前写的数据刷新到底层的传输协议中,例如:Socket 。 |
writeAndFlush |
先调用 write() ,然后调用 flush() 。 |
下图展示了如何利用 Channel.writeAndFlush()
实现最常见的发送数据到远端的任务。
Netty 的 Channel
实现是线程安全的,所以你可以存储 Channel
的一个引用,并且在你需要发送数据的时候使用它,即使在多线程环境中也没有关系。
包含的传输
下表展示了 Netty 中提供的传输。
名称 | 所在的包 | 描述 |
---|---|---|
NIO | io.netty.channel.socket.nio |
以 java.nio.channels 包为基础,是一个基于 selector 的实现方案。 |
Epoll | io.netty.channel.epoll |
通过 JNI 使用 epoll() 进入非阻塞 IO。这个传输的特性只适用于 Linux,如:SO_REUSEPORT 。它比 NIO 传输要快,是完全非阻塞。 |
OIO | io.netty.channel.socket.oio |
使用 java.net 包作为基础,使用阻塞流。 |
Local | io.netty.channel.local |
一个本地传输,可以被用来通过管道与 VM 交互。 |
Embedded | io.netty.channel.embedded |
一个嵌入式传输协议。通过它,你可以在虚拟的传输协议上使用 ChannelHandler 。这在测试 ChannelHandler 实现时很有用。 |
NIO — 非阻塞 I/O
Selector 背后基本的原理是:提供一个注册器,通过注册感兴趣的事件,当 Channel
状态改变的时候你可以得到通知。可能的状态改变包括:
- 新的
Channel
被接收到并且已经准备好。 -
Channel
连接已经完成。 -
Channel
已经准备好为读操作提供数据。 -
Channel
已经准备好,可以写入数据了。
下表展示了 java.nio.channels.SelectionKey
类定义的一些模式。
方法名称 | 描述 |
---|---|
OP_ACCEPT | 当一个新的请求被接受并且 Channel 被创建的时候,会进行通知。 |
OP_CONNECT | 当连接建立的时候,会进行通知。 |
OP_READ | 当可以从 Channel 中读取数据的时候,会进行通知。 |
OP_WRITE | 当可以往 Channel 中写入更多数据的时候,会进行通知。这个事件在 socket 缓存被填满的时候发生,通常意味着数据发送的速率操过了远端的处理能力。 |
NIO 内部实现的细节,在所有的 Netty 用户级别的 API 中都被隐藏了。图4.2 展示了处理的数据流。
零拷贝
零拷贝这个特性目前只在 NIO 和 Epoll 传输中被支持。通过它可以使你快速并高效地从一个文件系统转移数据到网络中,不需要从内核空间拷贝到用户空间,这大大提高了 FTP、HTTP 等协议的传输效率。这个特性并不是被所有的操作系统支持。确切地说,它并不适用于采用加密或压缩的文件系统,只有文件的原始数据可以被转移。但对于已经加密好的文件是可以被传输的。
Epoll — 针对 Linux 的原生非阻塞传输
Netty 为 Linux 系统提供了一个使用 epoll 的 NIO API。在某种程度上,这与 Netty 本身的设计更加一致,并且比中断的实现方式开销更少。如果你的应用准备在 Linux 上使用,那么可以考虑这个版本,你会发现在高负载的情况下,它的性能表现会好于 JDK 本生的 NIO 实现。
这个传输和图4.2中展示的传输语义相同,并且它的使用是很简单的,可以参考 list 4.4。为了使用 epoll 版的 NIO,可以将 NioEventLoopGroup
替换为 EpollEventLoopGroup
,将 NioServerSocketChannel.class
替换为 EpollServerSocketChannel.class
。
OIO — 阻塞型 I/O
你可能会对此感到好奇:Netty 是如何通过相同的 API 来支持异步传输的 NIO?答案是 Netty 使用了 SO_TIMEOUT Socket
标志。 这个标志表明等待 I/O 操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,那么将会抛出 SocketTimeoutException
。Netty 会捕捉这个异常,并继续循环处理。
Local — 通过本地传输与 JVM 交互
Netty 为同一个 JVM 上的客户端和服务端的异步交互提供了一个本地传输。
因为本地传输并没有真正的网络传输,所以它不能和其它传输协议一起协作。因此,一个客户端如果想通过这个传输来连接服务端的话(在同一个 JVM 上),服务端也必须使用这个传输。除了这个限制,它的使用和其它传输协议是一样的。
嵌入传输
Netty 还提供了一个传输,可以允许你嵌入在 ChannelHandler
中嵌入一个帮助类 ChannelHandler
。在这种模式下,你可以扩展 ChannelHandler
的功能,而不用内部的代码。
传输使用例子
下表展示了当前版本中传输支持的协议。
Transport | TCP | UDP | SCTP | UDT |
---|---|---|---|---|
NIO | 支持 | 支持 | 支持 | 支持 |
Linux 上的 Epoll | 支持 | 支持 | - | - |
OIO | 支持 | 支持 | 支持 |
在 Linux 上开启 SCTP
SCTP 需要内核支持并且需要安装一些用户库。例如,在 Ubuntu 上可以使用下面的命令:
# sudo apt-get install libsctp1
在 Fedora 上,你需要使用 yum:
# sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64
请阅读你的 Linux 发布版本的手册,了解如何开启 SCTP。
下面是你可能会遇到的一些用例。
- 基于非阻塞的代码:如果你在你的代码中不阻塞调用,或者你要限制阻塞的调用,推荐使用 NIO(在 Linux 上使用 epoll)。NIO/epoll 主要是用于处理很多并发的连接,在数量较少的时候变现的也不错,尤其是在多个连接共享线程的情况下。
- 基于阻塞的代码:正如我们之前提过,如果你的代码很依赖阻塞 I/O,并且你应用具有相应的设计,那么你直接从阻塞 I/O 切换到 Netty 的 NIO 传输,可能会遇到麻烦。最好不要直接重写代码来适应新的 I/O, 可以考虑一个阶段性的迁移:从 OIO 开始,迁移到 NIO(或者你使用 Linux 的话就是 epoll)。
- 在同一个 JVM 上进行交互:如果要在同一个 JVM 上进行交互不需要使用网络,使用本地传输再恰当不过了。这可以在使用 Netty 代码的同时消除真实网络的开销。如果你需要在真实网络上使用该服务,你只需将传输替换为 NIO 或者 OIO。
- 测试你的 ChannelHandler 实现:如果你想为你的
ChannelHandler
实现写单元测试,可以考虑使用嵌入传输。这可以使得你在测试实现的时候不用创建很多的模拟对象。你写的类还是会和通用的 API 事件流保持一致,保证ChannelHandler
在真实传输上表现正确。
下表总结了我们提到过的用例。
应用需求 | 推荐使用的协议 |
---|---|
非阻塞或者普通的尝试 | NIO(或者在 Linux 上使用 epoll) |
阻塞 | OIO |
在同一个 JVM 上交互 | Local |
测试你的 ChannelHandler 实现 | Embedded |