Netty实战四之传输

流经网络的数据总是具有相同的类型:字节(网络传输——一个帮助我们抽象底层数据传输机制的概念)

Netty为它所有的传输实现提供了一个通用的API,即我们可以将时间花在其他更有成效的事情上。

我们将通过一个案例来对传输进行学习,应用程序只简单地接收连接,向客户端写 “Hi!” ,然后关闭连接。

1、不通过Netty使用OIO和NIO

先介绍JDK API的应用程序的阻塞(OIO)版本和异步(NIO)版本。

这段代码完全可以处理中等数量的并发客户端,但是随着应用程序变得流行起来,你会发现它并不能很好地伸缩到支撑成千上万的并发连入连接。你决定改用异步网络编程,但是很快就发现异步API是完全不同的,以至于现在你不得不重写你的应用程序。

虽然这段代码所做的事情与之前的版本完全相同,但是代码却截然不同,如果为了用于非阻塞I/O而重新实现这个简单的应用程序,都需要一次完全重写的话,那么不难想象,移植真正复杂的应用程序需要付出什么样的努力!

2、通过Netty使用OIO和NIO

3、非阻塞的Netty版本

因为Netty为每种传输的实现都暴露了相同的API,所以无论选用哪一种传输的实现,你的代码都仍然几乎不受影响,在所有的情况下,传输的实现都依赖于interface Channel、ChannelPipeline和ChannelHandler。

4、传输API

传输API的核心是interface Channel ,它被用于所有的I/O操作。Channel类的层次结构如图4-1(Channel接口的层次结构)所示。

如图所示,每个Channel都将会将分配一个ChannelPipeline和ChannelConfig。ChannelConfig包含了该Channel的所有配置设置,并且支持热更新。

由于特定的传输可能具有独特的设置,所以它可能会实现一个ChannelConfig的子类型。

由于Channel是独一无二的,所以为了保证顺序将Channel声明为java.lang.Comparable的子接口。因此,如果两个不同的Channel实例都返回相同的散列码,那么AbstractChannel中的compareTo()方法的实现将会抛出一个Error。

ChannelPiepeline持有所有将应用于入站和出站数据以及事件的ChannelHandler实例,这些ChannelHandler实现了应用程序用于处理状态变化以及数据处理的逻辑。

ChannelHandler的典型用途包括:

-将数据从一种格式转换为另一种格式

-提供异常的通知

-提供Channel变为活动的或者非活动的通知

-提供当Channel注册到EventLoop或者从EventLoop注销时的通知

-提供有关用户自定义事件的通知

拦截过滤器 ChannelPipeline实现了一种常见的设计模式——拦截过滤器(InterceptingFilter)。UNIX管道是另外一个熟悉的例子:多个命令被链接在一起,其中一个命令的输出端将连接到命令行中下一个命令的输入端。

你也可以根据需要通过添加或者移除ChannelHandler实例来修改ChannelPipeline。通过利用Netty的这项能力可以构建出高度灵活的应用程序。例如,每当STARTTLS协议被请求时,你可以简单地通过向ChannelPipeline添加一个适当的ChannelHandler(SslHandler)来按需地支持STARTTLS协议。

考虑一下写数据并将其冲刷到远程节点这样的常规任务,代码清单4-5演示了使用Channel.writeAndFlush()来实现这一目的。

Netty的Channel实现是线程安全的,因此你可以存储一个到Channel的引用,并且每当你需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它。代码清单4-6展示了一个多线程写数据的简单例子,需要注意的是,消息将会被保证按顺序发送的。

5、内置的传输

Netty内置了一些可开箱即用的传输,因为并不是它们所有的传输都支持每一种协议,所以你必须选择一个和你的应用程序所使用的协议相容的传输。 下表显示了所有Netty提供的传输

6、NIO——非阻塞I/O

NIO提供了一个所有I/O操作的全异步的实现。它利用了自NIO子系统被引入JDK1.4时便可用的基于选择器的API。

选择器背后的基本概念是充当一个注册表,在那里你将可以请求在Channel的状态发生变化时得到通知。

-新的Channel已被接受并且就绪

-Channel连接已经完成

-Channel有已经就绪的可供读取的数据

-Channel可用于写数据

选择器运行在一个检查状态变化并对其做出相应响应的线程上,在应用程序对状态的改变做出响应之后,选择器将会被重置,并将重复这个过程。

零拷贝(zero-copy)是一种目前只有在使用NIO和Epoll传输时才可使用的特性。它使你可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间,其在像FTP或者HTTP这样的协议中可以显著地提升性能。但是,并不是所有的操作系统都支持这一特性。特别地,它对于实现了数据加密或者压缩的文件系统是不可用的——只能传输文件的原始内容。反过来说,传输已被加密的文件则不是问题。

7、Epoll——用于Linux的本地非阻塞传输

Linux作为高性能网络编程的平台,其重要性与日俱增,这催生了大量先进特性的开发,其中包括Epoll——一个高度可扩展的I/O事件通知特性,这个API自Linux内核版本2.5.44被引入,提供了比旧的POSIX select和poll系统调用更好的性能,同时现在也是Linux上非阻塞网络编程的事实标准。Linux JDK NIO API使用了这些epoll调用。

Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并且以一种更加轻量的方式使用中断,如果你的应用程序旨在运行于Linux系统,那么请考虑利用这个版本的传输,你将发现在高负载下它的性能要优于JDK的NIO实现。

8、OIO——旧的阻塞I/O

Netty的OIO传输实现代表了一种折中:它可以通过常规的传输API使用,但是由于它是建立在java.net包的阻塞实现上的,所以他不是异步的。

例如,你可能需要移植使用了一些进行阻塞调用的库(如JDBC)的遗留代码,而将逻辑转换为非阻塞的可能也是不切实际。相反,你可以在短期内使用Netty的OIO传输,然后再将你的代码移植到纯粹的异步传输上。

在 java.net API中,你通常会有一个用来接受到达正在监听的ServerSocket的新连接的线程。会创建一个新的和远程节点进行交互的套接字,并且会分配一个新的用于处理响应通信流量的线程。这是必需的,因为某个指定套接字上的任何I/O操作在任意的时间点上都可能会阻塞。使用单个线程来处理多个套接字,很容易导致一个套接字上的阻塞操作也捆绑了所有其他的套接字。

Netty是如何能够使用和用于异步传输相同的API支持OIO的呢?Netty利用了SO_TIMEOUT这个Socket标志,它指定了等待一个I/O操作完成的最大毫秒数。如果操作在指定的时间间隔内没有完成,则将会抛出一个SocketTimeout Exception。Netty将捕获这个异常并继续处理循环。在EventLoop下一次运行时,它将再次尝试,这也是Netty这样的异步框架能够支持OIO的唯一方式。

9、用于JVM内部通信的Local传输

Netty提供了一个Local传输,用于在同一个JVM中运行的客户端和服务器程序之间的异步通信,且也支持对于所有Netty传输实现都共同的API。

在这个传输中,和服务器Channel相关联的SocketAddress并没有绑定物理网络地址;相反,只要服务器还在运行,它就会被存储在注册表里,并在Channel关闭时注销。因为这个传输并不接受真正的网络流量,所以它并不能够和其他传输实现进行互操作。因此,客户端希望连接到(在同一个JVM中)使用了这个传输的服务器端时也必须使用它。除了这个限制,它的使用方式和其他传输一模一样。

10、Embedded传输

Netty提供了一种额外的传输,使得你可以将一组ChannelHandler作为帮助器类嵌入到其他的ChannelHandler内部。通过这种方式,你将可以扩展一个CHannelHandler的功能,而又不需要修改其内部代码。

11、传输的用例

在Linux上启用SCTP

SCTP需要内核的支持,并且需要安装用户库

例如,对于Ubuntn,可以使用下面的命令

sudo apt-get install libsctpl

对于Fedora,可以使用yum

sudo yum install kernel-modules-extra.x86_64 lksctp-tools.x86_64

有关如何启用SCTP的详细信息,请参考你的Linux发行版的文档。

虽然只有SCTP传输有这些特殊要求,但是其他传输可能也有它们自己的配置选项需要考虑。此外,如果只是为了支持更高的并发连接数,服务器平台可能需要配置得和客户端不一样。

——非阻塞代码库:如果你的代码库中没有阻塞调用(或者你能够限制它们的范围),那么在Linux上使用NIO或者epoll始终是个好主意。虽然NIO/Epoll旨在处理大量的并发连接,但是在处理较小数目的并发连接时,它也能很好地工作,尤其是考虑到它在连接之间共享线程的方式。

——阻塞代码库:如果你的代码库严重地依赖于阻塞I/O,而且你的应用程序也有一个相应的设计,那么在你尝试将其直接转换为Netty的NIO传输时,你将可能会遇到和阻塞操作相关的问题。不要为此而重写你的代码,可以考虑分阶段迁移:先从OIO开始,等你的代码修改好之后,在迁移到NIO(或者EPoll,如果你在使用Linux)

——在同一个JVM内部的通信:同一个JVM内部的通信,不需要通过网络暴露服务,是Local传输的完美用例。这将消除所有真实网络操作的开销,同时仍然使用你的Netty代码库。如果随后需要通过网络暴露服务,那么你将只需要把传输改为NIO或者OIO即可。

——测试你的ChannelHandler实现:如果你想要为自己的ChannelHandler实现编写单元测试,那么请考虑使用Embedded传输。这既便于测试你的代码,而又不需要创建大量的模拟对象。你的类将仍然符合常规API事件流,保证该Channelhandler在和真实的传输一起使用时能够正确地工作。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180302G0Q3IU00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券