网络编程的基本模型是 Client/Server 模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的 IP 地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
在基于传统同步阻塞模型开发中,ServerSocket 负责绑定 IP 地址,启动监听端口;Socket 负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
首先,我们通过图 2-1 所示的通信模型图来熟悉下 BIO 的服务端通信模型:
采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
图1-1同步阻塞 1/0服务端通信模型(1客户端1线程)
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈 1:1 的正比关系,由于线程是 Java 虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。
为了解决同步阻塞 I/O 面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数 M:线程池最大线程数 N 的比例关系,其中 M 可以远远大于 N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
下面,我们结合连接模型图和源码,对伪异步 I/O 进行分析,看它是否能够解决同步阻塞 I/O 面临的问题。
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如图 1-2 所示。
当有新的客户端接入的时候,将客户端的 Socket 封装成一个 Task(该任务实现 java.lang.Runnable 接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
图 1-2 伪异步I/0服务端通信模型(M: N)
伪异步 I/O 实际上仅仅只是对之前 I/O 线程模型的一个简单优化,它无法从根本上解决同步 I/O 导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长,会引起的级联故障。
1. 服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms。
2. 采用伪异步I/O的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60s。
3. 假如所有的可用线程都被故障服务器阻塞,那后续所有的I/O消息都将在队列中排队。
4. 由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞。
5. 由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时。
6. 由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。
在介绍 NIO 编程之前,我们首先需要澄清一个概念:NIO 到底是什么的简称?
有人称之为 New I/O,因为它相对于之前的 I/O 类库是新增的,所以被称为 NewI/O,这是它的官方叫法。但是,由于之前老的 I/O 类库是阻塞 I/O,New I/O 类库的目标就是要让 Java 支持非阻塞 I/O,所以,更多的人喜欢称之为非阻塞 I/O(Non-block I/O),由于非阻塞 I/O 更能够体现 NIO 的特点,所以本文使用的NIO 都指的是非阻塞 I/O。
与 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞 I/O 以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。
NIO2.0 引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。异步通道提供两种方式获取获取操作结果:
NIO2.0 的异步套接字通道是真正的异步非阻塞 I/O,它对应 UNIX 网络编程中的事件驱动 I/O(AIO),它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了 NIO 的编程模型。
不同的 I/O 模型由于线程模型、API 等差别很大,所以用法的差异也非常大。
由于之前的几个小节已经集中对这几种 I/O 的 API 和用法进行了说明,本小节会重点对这几种 I/O 进行功能对比。如表 2-1 所示。
表 1-1 几种 I/O 模型的功能和特性对比
随着移动互联网的发展和大数据时代的到来,大规模分布式服务框架、分布式流计算框架已经成为架构主流,分布式服务节点之间的通信形式往往是内部长连接,例如 FaceBook 的 Thrift 协议,为了提升节点间的通信吞吐量、提升通信性能,目前主流的内部通信框架均使用 NIO 框架,对于大公司、技术积累比较深的团队可能会使用自研的 NIO 框架来满足个性化或者行业特殊的需求,但是大多数架构师会选择业界主流的 NIO 框架进行异步通信开发。
目前,业界主流的 NIO 框架主要有两款:Mina 和 Netty,两者都使用 ApacheLICENSE-2.0 进行开源。不同之处是 Mina 是 Apache 基金会的官方 NIO 框架,Netty 之前是 Jboss 的 NIO 框架,后来脱离 Jboss 独立申请了 netty.io 域名,与 Jboss 脱离关系,并对版本进行了重构,导致 API 无法向上兼容。
Mina 和 Netty 还 有一段 历 史 渊 源,Mina 最 初 版 本 的 架 构 师 是 TrustinLee,后来,由于种种原因,Trustin Lee 离开了 Mina 社区加入到了 Netty 团队,重新设计并开发了 Netty。很多读者会发现 Netty 中透着 Mina 的影子,两个框架的架构理念也有很多相似之处,甚至一些代码都非常相似,原因就在这里。
目前,Mina 和 Netty 的应用已经非常广泛,很多开源框架都使用两者做底层的 NIO 框架,例如 Hadoop 的通信组件 Avro 使用 Netty 做底层的通信框架,
Openfire 则使用 Mina 做底层通信框架,相比于 Mina,Netty 社区目前更活跃,版本应用范围也更广。
现在我们总结一下为什么不建议开发者直接使用 JDK 的 NIO 类库进行开发,具体原因如下。
1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
2. 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。
3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
4. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。该BUG以及与该BUG相关的问题单可以参见以下链接内容。
由于上述原因,在大多数场景下,不建议大家直接使用 JDK 的 NIO 类库,除非你精通 NIO 编程或者有特殊的需求。在绝大多数的业务场景中,我们可以使用NIO 框架 Netty 来进行 NIO 编程,它既可以作为客户端也可以作为服务端,同时
支持 UDP 和异步文件传输,功能非常强大。
Netty 是业界最流行的 NIO 框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如 Hadoop 的 RPC 框架 avro 使用 Netty 作为底层通信框架;很多其他业界主流的 RPC 框架,也使用 Netty 来构建高性能的异步通信能力。
通过对 Netty 的分析,我们将它的优点总结如下 :
正是因为这些优点,Netty 逐渐成为 Java NIO 编程的首选框架。
Netty 的架构图如下所示。
首先假设你已经在本机安装了 JDK1.7,配置了 JDK 的环境变量 path,同时下载并正确启动了 IDE 工具 Eclipse。如果你是个 Java 初学者,从来没有在本机搭建过 Java 开发环境,建议你先选择一本 Java 基础入门的书籍或者课程学习。
假如你习惯于使用其他 IDE 工具进行 Java 开发,例如 NetBeans IDE,也可以运行本节的入门例程。但是,你需要根据自己实际使用的 IDE 进行对应的配置修改和调整,本书统一使用 eclipse-jee-kepler-SR1-win32 作为 Java 开发工具。
访问 Netty 的官网 http://netty.io/,从【Downloads】标签页选择下载4.1.5.Final 软件包,包含了源码、编译类库和 Java Doc,18.1M 左右,解压之后的软件包如下所示。
这时会发现里面包含了各个模块的.jar 包和源码,由于我们直接以二进制类库的方式使用 Netty,所以只需要获取 netty-all-4.1.5.Final.jar 即可。
将 netty-all-4.1.5.Final.jar 导入到 Java 工程的 lib 目录下(lib 目录需要自建),右键单击 netty
-all-4.1.5.Final.jar,在弹出的菜单中,选择将.jar包添加到 Build Path 中,即可完成 Netty 开发环境的搭建。