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 所示 。
( 2 ) 非阻塞 1/0 模型 :recvfrom 从应用层到 内核的时 候 ,如果该缓冲区没有 数据的话 , 就直接返回一个 EWOULDBLOCK 错误 ,一 般都对非阻塞 1/0 模型进行轮询检 查这个状态 , 看 内核是不是有数据到 来。如 图 1-2 所 示 。
( 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 所示 。
( 4 ) 信号驱动 I/0 模型:首先 开启套接 口信号驱动 l/0 功能,并通 过系统调用 sigaction 执行 一个信号处理 函数 〈 此系统调用立 即返回,进程 继续工作 ,它是非阻塞的 〉 。当数据 准备就绪时 ,就 为 该进 程生成一个 SIGIO 信号 ,通过信号回调通知应用程序 调用 recvfrom 来读取数据 , 并通知主循环 函数处理,数据 。如图 1-4 所示 。
( 5 ) 异步 1/0:告知 内核 启动某个操作 ,并让 内核在整 个操作完成后 〈 包括将数据从 内核复制到用户 自己的缓冲 区〉 通知我们 。这种模型与信 号驱动模型的主要 区别是 :信号 驱动 1/0 由内核通知我们何时可 以开 始一个 1/0 操作 :异步 1/0 模型白内核通知我们 I/0 操作何 时 已经完成 。如 图 l -5 所示 。
如果想要了解 更 多的 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 的优点有更加深刻的体会 。