(基础篇 走进javaNIO)第一章-java的i/o演进之路

Java 是由 SUN公司在 1995 年首先发布 的编程语 言和计算平 台。这基础技术 支持最新 的程序 ,包括 实用程序 、游 戏和业 务应用程序 。J ava 在世界各地 的 8.5  亿 多 台个 人计算机和数 十亿 套设备上运行着 ,其l扫 包括移动设备和 电视设备 。

Java 之所 以能够得到如此广泛 的应用 ,除 了摆脱硬件平 台的依赖具有 “ 一次编写 、到 处运行 ” 的平 台无关性特性之外 ,另一个 重要原因是 :其丰富而强大的类库 以及众 多第三 方开源类库使得基于 Java 语 言的开发更加简单和便捷 。

伺是 ,对 于一些经验丰富的程序员来说 ,Java 的一些类库在早 期设计 巾功 能并不完善 或者在在一些缺 陷,其中最令人恼火 的就是基于同步 1/0 的 Socket 通信类库 ,直到 2002 年 2 月 13 日 J OK I .4 Merlin 的发布 ,J ava 才第一次支持非阻塞 I/0,这 个类库的提供 为 JDK 的通信模型带 米了翻天覆地 的变化 。

在开始学 java之前 ,我们首先对 UNIX 系统常用的 I/0 模型进行介 绍 ,然后对 Java 的 1/0 历史演进行简单说明 。通过本章节的 学习,希望读者对同步和异步 l/0  以及 Java  的 l/0  类库发展有个直观的了解 ,方便后续章节 的学 习。如果你己经熟练 NIO  编程或者 从事过 UN IX  网络编程? 希望直撞学习 Java 的 NIO 和 Netty,那就可以直接跳到第 2 章进行学习 。

本章主要 内容包括 :

I/0   基础入 门

Java 的 1/0 演进


1.1   1/0基础入门

Javal1.4 之 前的早期版本 ,Java 对IO的支持并不完善 ,开发人员在开发高性能 l/0程序的时候,会面临一些巨大的挑战和困难 ,主要问题如下

没有数据缓冲区,i/0 性 能存在 问题;

没有C 或者 C++中的 channel概念,只有输入和输出流 ;

同步阻塞式 i/0 通信( BIO通常会导致通信线程被长时间阻塞 ;

支持的字符集有 限,硬件可移植性不好;

在 Java 支持异步 i/0 之前的很长一段 时间里 ,高性 能服务端开发领域一直被 C++和 C长 期占据,J ava 的同步阻塞 i/0 被大家所记病 。

1.1.1     Linux 网络 1/ 0 模型简 介

Linux 的内核将所有外部设备都看做一个文件来操作 ,对一个文件 的读 写操作会调用 内核提供的系统命令 ,返回一个 file descriptor ( fd,文件描述符 〉 。而对 一个 socket  的读 写 也会有相应 的描述 符 ,称 为 socketfd ( socket 描述符 ) ,描述 符就是一个数字 ,它指 向内 核中 的一个 结构体 ( 文件路径 ,数 据区等一些属性 〉 。

根据 UN IX   网络编程对 IO模型的分类 ,UNIX 提供了 5 种 1/0 模型 ,分别如 下。

( 1 ) 阻塞 I/0 模型 :最常用的 1/0 模型就是阻塞I/0模型,缺省情形下,所有文件操作都是阻塞的。我们以套接宇接 口为例来讲解此模型:在进程空间中调用recvfrom,其系 统调用直到数据’包到达且被复制剑应用进程的缓 冲 区中或者发 生错误时才返回 ,在此期 间 一直会等待 ,进程在从调用 recvfrom  开始到它返回的整段时间内都是被 阻塞的 ,因此被称 为 阻裴 1/0 模型 ,如图 1 - 1  所示 。

(基础篇 走进javaNIO)第一章-java的i/o演进之路

( 2 ) 非阻塞 1/0 模型 :recvfrom    从应用层到 内核的时 候 ,如果该缓冲区没有 数据的话 , 就直接返回一个 EWOULDBLOCK 错误 ,一 般都对非阻塞 1/0 模型进行轮询检 查这个状态 , 看 内核是不是有数据到 来。如 图 1-2 所 示 。

(基础篇 走进javaNIO)第一章-java的i/o演进之路

( 3 ) I/0 复用模型 :Lin ux 提供 select/poll ,进程通过 将 一个或多个 fd传给 select 或 poll 系统调用 ,阻塞在 select 操作上 ,这样 select/poll 可 以帮我们侦测多个 fd 是否处 于就 绪状态 。select/pol l 是顺序扫描 fd是否就绪 ,而且支持 的 fd数量有限 ,因此它的使用受到 了一些制约 。Linux且 还提供了一个 epoll 系统调用 ,epoll 使用基于事件驱动方式代 替顺序 扫描 ,因此性 能更高 。当有 fd就绪时 ,立 即回调函数 rollback 。如 图 1-3 所示 。

(基础篇 走进javaNIO)第一章-java的i/o演进之路

( 4 ) 信号驱动 I/0 模型:首先 开启套接 口信号驱动 l/0 功能,并通 过系统调用 sigaction 执行 一个信号处理 函数 〈 此系统调用立 即返回,进程 继续工作 ,它是非阻塞的 〉 。当数据 准备就绪时 ,就 为 该进 程生成一个 SIGIO 信号 ,通过信号回调通知应用程序 调用 recvfrom 来读取数据 , 并通知主循环 函数处理,数据 。如图 1-4 所示 。

(基础篇 走进javaNIO)第一章-java的i/o演进之路

( 5 ) 异步 1/0:告知 内核 启动某个操作 ,并让 内核在整 个操作完成后 〈 包括将数据从 内核复制到用户 自己的缓冲 区〉 通知我们 。这种模型与信 号驱动模型的主要 区别是 :信号 驱动 1/0  由内核通知我们何时可 以开 始一个 1/0 操作 :异步 1/0 模型白内核通知我们 I/0 操作何 时 已经完成 。如 图 l -5 所示 。

(基础篇 走进javaNIO)第一章-java的i/o演进之路

如果想要了解 更 多的 UNIX  系统 网络编程知识 ,可以阅 读 《UNIX  网络编程》 ,里面

非常详细 的原理和 API 介绍 。对于 大多数 Java 程序 员来说不需要 了解网络编程 的底层 细节 ,大家只需要有个概念 ,知道对 于操作系统而言,底层 是支持异步 I/0 通信 的 ,只不 过在很长一段时间 Java 并没有提供异步 I/0 通信的类库 ,导致 很多原生的 Java 程序员对 这块 儿 比较 陌生 。当你了解 了网络编程 的基础知识后,理解 Java 的 NIO 类库就会更加 容 易一些。

下一个小结我们重点讲下  1/0  多路复用技术 ,因为 Java NIO 的核心类库 多路 复用器Selector 就是基于 epoll 的多路复用技术实现 。


1.1.2   i/ 0 多路复用技术

在 I/0 编程过程 中,当需要 同时处理多个客户端接入请求时,可以 利用多线程或者 I/0 多路复用技术进行处理 。I/0 多路复用技术通过把 多个 1/0 的阻塞复用到同一个 select 的阻 塞上 ,从而使得 系统在单线程的情况下可 以同时处理多个客户端请求 。与传统的 多线程/ 多进程模型比 ,I/0多路复用 的最大优势是系统开销小 ,系统不需要创建新 的额外进程或 者线程 ,也不需要维护这些进 程和线程 的运行 ,降低了系统 的维护工作量,节省了 系统资 源 ,I/0 多路复用的主 要应用场景如下 。

1.服务器需要 同时处理 多个处于监昕状态或者多个连接状态的套接字

2.服务器需要 同时处理多种网络协议 的套接字

目前支 持 1/0 多路复用的 系统 调用有 select 、pselect 、poll 、cpoll,在 Linux  网络编程 过程 ,很长 一段时间 都使用 select 做轮询和 网络事件通知 ,然 而 select  的一些固有缺陷 导致 了它的应用受到 了很大的限制,最终 Linux  不得不在新 的内核版本 中寻找 select  的替 代方 案 ,最终选 择 了epoll,epoll 与select 的原理比较类似了克服 select 的缺点,epoll作 似多重大改进现总结如下。

1. 支持一个进程打开的 socket描述得 (FD)不受限制 ( 仅受限于操作系统的最大文件句柄数 )。

sel ect最 大的缺陷就是单个进程所打开的 FD是有一定 限制的 ,它由FD_SETSIZE 设 置 ,默认值是 1024 。对 于那些需要支持上万个 TCP 连接 的大型服务器来 说显然太少了 。 可以选择修改这个宏然后重新编译内核 ,不过这会带来网络效率 的下降。我们 也可 以通过 选择 多进程的方案 ( 传统的 Apache 方案 〉 解决这个问题 ,不过虽 然在 Linux  上创建进程 的代价 比较 小 ,但 仍 旧是不可忽视 的,另外 ,进程间 的数据交换 非常麻烦 ,对Java 由 于没有共享 内存 ,需要通 过 Socket 通信或者其他方式进行数据同 步 ,这带 来 了额外 的性 能 损耗 ,增加 了程序复杂度 ,所 以也不是一种完美 的解决方案 。值得庆幸 的是 ,epo ll 并没有 这个 限制,它所 支持的 FD 上 限是操作系统的最 大文件句柄数 ,这 个数字远远大于 1024 。例如 ,在 1GB 内存 的机器上 人约是 10 万个句柄左右 ,具体 的值 可 以通过 cat /proc/sys/fs/file­ max 察看 ,通 常情况下这 个值跟系统 的 内存关系比较 大。

2.   1/0 烈率不会随着 FD 数目的增加而线性下降。

传 统的 select/poll  另一个致命弱 点就是 当你拥有一个很大的 socket 集合 ,由于 网络延 时或者链路空闲,任 一时刻只有少部分的 socket 是 “ 活跃 ” 的,但是 select/poll 每次调用 都会线性扫描全部的集合 ,导致效率呈现线性 下降。epoll  不存在这个 问题 ,它只会 对 “ 活 跃 ” 的 socket 迸行操作 这是因 为 在 内核实现 中,i  epoll  是根据每个 “ 上面 的 callback  函数实 现 的,那么 ,只有 “ 活跃 ” 的 socket 才会主动的去调用 callback  函数 ,其他 id le 状态 socket 则不会 。在这 点上 ,epoll  实现 了一个伪 AIO 。针对 epoll  和 select  性 能对 比的 benchmark 测试表 明:如 果所有 的 socket   都 处 于活跃态 例如一个 高速 LAN   环境 ,epoll 并不 比 select/po11 效率高太多 ;相 反 ,如果过 多使用 epoll_ctl ,效率相比还 有稍微 的下降。但 是一 旦使用 idle connections 模拟 WAN  环境 ,epoll 的效率就远在 select/poll 之上了 。

3 .   使用 mmap 加速内极与用户空间的消息传递 。

无论 是 select ,     poll还是 epoll 都需要内核 把 FD 消息通知给用户空间 ,如何避 免不必耍 的内存复制就显得非常重要 ,epoll 是通过内核和 用户空间 mmap 同一块 内存 实现 。

4.   epoll 的 API 更加简单。

包括创建一个 epoll  描述符 、添加监听事件 、阻塞等待所监听的事件发生, 关闭 epoll描述符等 。

到这里 ,1/0 的基础知识已经介绍完毕 ,从 1.2 章节开始介绍 Java 的 I/0 演进 历史

从 BIO 到 NIO 是 Java 通信类库迈 出的一小步,但却对 Java 在高性 能通信领域的发展起 到 了关键性 的推动作用 。随着基于 NIO 的各类 NIO 框架的发展 ,以及基于 NIO 的 Web 服务 器的发展 ,Java 在很多领域取代了 C 和 C++,成 为企业服务端应用开发 的首选语言。


1.2    Java 的 1/0 演进

在 JDK  1.4 推 出 Java  NIO 之前 ,基于 Java  的所有 Socket 通信都采用了同步阻塞模式 ( BIO ) ,这种一请求一应答的通信模型简化 了上层的应用开发 ,但 是在性能和可靠性方面 却存在着 巨大的瓶颈 。因此 ,在很长一段 时间里 ,大型的应 用服务器都采用 C  或者 C++ 语言,因为 它们 可 以直接使用操作系统提供 的异步 J/0 或者 AIO 能力 。当并发访 问量 增大 、响应时间延迟增大之 后 ,采用 Java BIO 开发的服务端软件只有通过硬 件的不 断扩容 来满足 高并发和低时延 ,它极大地 增加 了企业 的成本 ,并且 随着集群规模 的不断膨胀 ,系 统的可维护性也面临巨大的挑战 ,只能通过采购性 能更高的硬件服务器来解决问 题 ,这会 导致恶性循环 。

正是由于Java传统BIO的拙劣表现才使得 Java 支持非阻塞 I/0 的呼声日渐高涨,最终,JDK1.4 版本提供了新 的 NIO 类库 ,Java 终于也可 以支持非阻塞 l/0 了。

Java 的 1/0 发展简史

从 JDK1.0 到 JDK 1.3 , Java 的 I/0 类库都非常原始 ,很多 UNIX 网络编程中的概念或 者接口在 I/0 类库中 都没有体现 ,例 如Pipe ,Channel 、Buffer 和 Selector 等 。2002 年发布 JDK l1.4 时,N IO 以 JSR-51 的身份正式随 JDK 发布 。它新增了个 java.nio 包 ,提供了很 多 进行异步 1/0 开发的 API 和类库 ,主要的类和 接口如下。

1.进行异 步 1/0 操作的缓冲 区 ByteBuffer 等 ;

2.进行异步 1/0 操作 的管道 Pipe ;

3.进行各种 1/0 操作 ( 异步或者 同步 ) 的 Channel ,包括 ServerSocketChannel和SocketChannel :

4.多种字符集 的编码能力和解 码 能力;

5.实现非阻塞 I/0 操作的多 路复用器selector ;

6.基于流行 的 Perl  实现 的正则表达式类库 ;

7.文件通道 FileChannel 。

新的N IO 类库的提 供 ,极大地促进 了基于 Java 的异步非阻塞编程的发展和应用,但是 ,它依然有不完善 的地方 ,特别是 对文件系统的 处理能力仍显不足 ,主要 问题如下。

1.没有统一的文件属性 〈 例如读 写权 限);

2.API 能力 比较弱 ,例 如目录的级联创建和递归遍历 ,往往需要 自己实现 ;

3.底层存储系统的 一些高级 API 无法使用:

4. 所有的文件操作都 是 同步阻塞调用 ,不支持 异步文件读写操作 。

2011年 7 月 28 日,JDK 1.7 正式发布 。它的一个比较 大的亮点就是将原来 的 NIO 类 库进行了升 级 9  被称为 NI02.0。NI02.0  由 JSR-203 演进而来 ,它主要提供 了如下三个方 面 的改进 。

1.提供能 够批量获取文件属性 的 API ,这些 API 具有平 台无关性 ,不与特性 的文件 系统相耦合 ,另外它还提供 了标准文件系统的 SPI ,供各 个服务提供商扩展实现

2.提供 AIO 功能 ,支持基于文件的异步 I/0 操作和针对网络套接字的异步操作

3.完成 J SR-5 1  定义的通道功 能 ,包括对配 置和多播数据报的支持等


1.3     总结

通过本 章的学习,我们 了解了unix网络编程 的 5 种 1/0 模型 ,学习了I/0 多路复用 技术的 基础知识 。通过对 Java i/0 演进历史的总结和介绍 ,相信大 家对 Java. 的I/0 演进有 了一个更加直观 的认识 。后面的第 2 章节会对阻 塞 1/0 和非阻塞 1/0 进行 详细讲解 ,同时 给 出代码示例 。相信学完第 2 章之后 ,大家就能够对传统的阻塞 IJO  的弊端和非阻 塞I/0 的优点有更加深刻的体会 。

上一篇:python全栈开发-Day5 元组、字典


下一篇:转《UNIX编程艺术》读书心得