本文是Netty文集中“Netty in action”系列的文章。主要是对Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一书简要翻译,同时对重要点加上一些自己补充和扩展。
概要
- OIO —— 阻塞传输
- NIO —— 异步传输
- Local transport —— JVM内部的异步通讯
- Embedded transport —— 测试你的ChannelHandlers
数据流经一个网络时总是有一样的类型:字节。
使用JAVA提供OIO API 和 NIO API 有着很大的不同。
Netty使用了一个公共的API层,该API涵盖了所以的传输实现
在Netty中使用OIO 和 NIO
传输协议API
传输API的关键是 Channel 接口,Channel接口被用于所有的I/O操作。
一个Channel会被分配有一个ChannelPipeline和一个ChannelConfig。
ChannelConfig持有所有设置Channel的配置并支持热修改。因为一个指定的传输可能有它独特的设置,它可以实现一个ChannelConfig的子类。
因为Channel都是独一无二的,所以声明Channel为java.lang.Comparable的子类用意是为了保证排序。因此,AbstractChannel对compareTo方法实现:当两个不同的channel实例返回了相同的hashCode将抛出一个Error异常。
ChannelPipeline持有所以的ChannelHandler实例,这些ChannelHandler实例将被应用到入站和出站数据和事件上。这些ChannelHandlers实现了用于处理状态改变和数据处理的应用逻辑。
典型的ChannelHandlers的使用包括:
- 转换数据格式从一种到另外一种
- 提供异常的通知
- 提供一个Channel活跃( active )或不活跃( inactive )的通知
- 提供当一个Channel注册( registered )到EventLoop或从EventLoop注销( deregistered )的通知
- 提供关于用户定义事件的通知
Intercepting Filter :ChannelPipeline实现了一个常见的设计模式,拦截过滤器。UNIX 的管道是另一个常见的例子:指令被链接到一起,通过一个指令的输出连接到下一个行的输入。( 也就是将当前指令的输出作为下一条指令的输入内容,以此方式将指令给链接到一起 )
你可以通过需要添加或删除ChannelHandler来即时修改ChannelPipeline。Netty的这个能力能被利用与构建一个高灵活性的应用。
Netty的Channel实现是线程安全的,所以你能够存有一个Channel的引用,并在你需要的任何时候使用它去写数据到远端,甚至可以多个线程同时使用这个引用。
包含的传输协议
NIO —— 非阻塞 I/O
NIO提供所有I/O操作的完全异步实现。它使用了基于selector的API。
selector的一个基本概念是作为一个注册表,你请求收到一个通知当Channel的状态改变时。
可能的状态改变有:
- OP_ACCEPT :个新Channel被接收并准备好 ( 服务端 )
- OP_CONNECT :一个Channel连接已经完成 ( 客户端 )
- OP_READ :一个Channel的数据已经准备好被读取
- OP_WRITE :一个Channel的写数据有效。
OP_WRITE需要特别注意。该事件表示的是:请求收到通知,当Channel能够写入更多的数据时。这是当socket缓存已经完全满的处理情况( 即,当socket缓存已经满了,但还有数据未写完时,需要注册该事件为希望得到通知的事件 ),这经常发生在当数据的传输速度远快于远端处理数据的速度时。
在应用对状态的改变作出反应后,selector将被重置,并且重复该过程。
这些模式被合并到一个指定的集合中,应用请求得到一个通知当该集合中包含的状态改变时。
这些NIO的内部实现被用户级API所隐藏,该API是Netty所有传输的共同实现。
零拷贝是目前仅适用于NIO和Epoll传输的功能。它允许你 快速且高效的移动数据从一个文件系统到网络,而无需从内核空间拷贝数据到用户空间,这能够显著提升如FTP 或 HTTP协议的性能。零拷贝功能并不是所有的操作系统都支持的。需要指明的零拷贝不能用于实现文件系统的数据加密或压缩,它只能够传输未加工的文件内容。相反的,传输一个已经被加密过的文件不是问题。
也就是说,有些文件系统不是单纯的操作一个数据的传输,还要对文件进行一些加密和压缩的操作,而这些需要将数据拷贝到用户空间并对数据进行修改操作。所以像这样的文件操作是不支持零拷贝的。
Epoll —— Linux的本地非阻塞传输
正如我们前面说展示的,Netty的NIO传输是基于java提供的异步/非阻塞网络的通用抽象。尽管这确保了Netty的NIO能在任何平台上使用;但它也有限制,因为JDK必须妥协才能让所有的系统都具有相同的功能。
Linux作为日渐重要的高性能网络平台,这导致了许多先进功能的开发,包括epoll,一个高可扩展的I/O事件通知功能。
Netty为Linux提供了一个使用epoll的NIO API,通过该方式与你的设计更加一致并且使中断的使用成本更低。在大负载的性能上,Linux NIO 实现优于JDK NIO 的实现。
OIO —— 老的阻塞 I/O
Netty OIO传输实现代表着一种妥协:它通过通用的传输API来访问,但因为他构建在java.net的阻塞实现上,它是非异步的。它非常适用于某些情况。鉴于此,你可能担心Netty如何提供一个NIO通过一样的API用于异步的传输。这个答案是Netty使用 SO_TIMEOUT Socket 标志,该标志指定了等待I/O操作完成的最大毫秒数。如果一个操作在指定期间内没有完成,那么将抛出一个SocketTimeoutException异常。Netty捕获这个异常并继续处理循环。在下一次EventLoop运行时,将再尝试一次前面的逻辑。这是一个像Netty的异步框架能够支持OIO的唯一方式。
我们通过OioSocketChannel的读操作来了解下关于上面描述的源码实现:
👆这个读操操作如果抛出超时异常,则会返回读到的字节数为0。这里大家可以关注另外一点,在当socket关闭是,返回时可读字节数为-1。这个是和NIO的模式相一致的,在NIO中如果read返回的可读字节数为-1时,也就表示当远端连接已经关闭了。
用于JVM内部通讯的本地传输
Netty提供了一个本地传输用于客户端和服务端在相同JVM的异步通讯。
在该传输中,一个同服务端Channel关联的SocketAddress不会绑定到一个物理网络地址;当然,它会被保存到一个注册表在服务端运行的期间,并在Channel ( 这里指服务端的channel )关闭时被注销。所以传输没有通过真实的网络传输,所以它不能通过其他传输的实现来进行交互 ( 也就是不能同其他传输,如NIO transport 进行数据的传输交互 )。所以客户端希望连接一个在同一JVM的使用了该传输方式的服务端,那么客户端也需要使用该传输方式。除了这个限制,它与其他传输方式并无不同。
内嵌的传输协议
Netty提供了一个附加的传输方式,该传输方式允许你一个ChannelHandler作为辅助类嵌入到其他ChannelHandler中。照这样,你能在不修改内部代码的情况下够扩展一个ChannelHandler的功能。
传输协议使用场景
并不是所有的传输方式都支持所有的传输协议。这里是你可能会遇到的使用场景:
- 非阻塞代码库 —— 如果你不要一个阻塞调用在你的代码库中,或者你能够限制它们,在Linux上使用NIO或epoll经常是个好主意。当NIO/epoll 用于处理许多并发的连接,它也能通过更少的线程来更好的工作,尤其是在连接间共享线程的方式。
- 阻塞代码库 —— 正如我们已经说到的,如果你的代码库严重依赖于阻塞I/O,并且你的应用有对应于此的设计。如果你直接转为Netty的NIO传输方式,你可能会遇到阻塞操作问题。对比与重写你的代码去完成这些,考虑一个阶段性的迁移:从OIO开始,然后转移到NIO(或epoll如果你在Linux上)当你改进你的代码后。
- 相同JVM的内部通讯 —— 在相同JVM的内部通讯不需要暴露一个服务在网络表现层,在相同JVM的内部通讯为本地传输的完美使用情况。这将消除真实网络操作的所有开销,同时仍然使用你的Netty代码库。如果需要暴露一个服务在网络上,你只需要简单的修改传输方式为NIO或OIO。
- 测试你的ChannelHandler的实现 —— 如果你想要写单元测试用于你的ChannelHandler实现,考虑使用内嵌的传输方式。这将使测试你的代码变得简单,而不需创建许多的mock对象。你的类将仍然遵循通用API的事件流,保证ChannelHandler将在真实传输中正确工作。
后记
本文主要对Netty的支持的传输协议进行了介绍。即便是不同的传输协议,Netty也为我们提供了一致的API接口,它将大量复杂的处理逻辑封装在了源码实现中,为用户提供了简易且方便的API接口,这也是Netty设计一致性的例子之一。
若文章有任何错误,望大家不吝指教:)