04 传输

点击查看 《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 中组合了 ChannelPipelineChannelConfigChannelConfig 保存了 Channel 所有的配置,并且支持热更新。

由于 Channel 是独一无二的,所以 Channel 继承了 java.lang.Comparable 接口,从而保证对象的有序。因此,如果两个 Channel 实例返回相同的 hash 值,那 AbstractChannelcompareTo() 的实现将会抛出 Error

图4.1 Channel接口继承关系

ChannelHandler 常用于:

  • 转换数据格式。
  • 提供异常通知。
  • 提供 Channel 被激活或者关闭的通知。
  • 提供 ChannelEventLoop 中注册和取消注册的通知。
  • 提供用户定义事件的通知。

拦截过滤器
ChannelPipeline 实现了一个常用设计模式:拦截过滤器。UNIX 管道是这个模式的另一个典型例子:命令被链式编排,每个命令的输出会作为接下来一个命令的输入。

你同样可以在运行时按照你的需求为 ChannelPipeline 添加或删除 ChannelHandler

除了可以使用 ChannelPipelineChannelConfig 之外,你还可以使用 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 展示了处理的数据流。

图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

图4.3 OIO 处理逻辑

你可能会对此感到好奇: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 上可以使用下面的命令:

1
# sudo apt-get install libsctp1

在 Fedora 上,你需要使用 yum:

1
# 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
-------------本文结束感谢您的阅读-------------