NIO
新的输入/输出库,是在 JDK1.4
中引入的。NIO 弥补了原来的 I/O 的不足,它在标准的Java代码中提供了 高速的、面向块
的I/O。
NIO的创建目的 是为了让Java程序员可以实现高速I/O而无需编译自定义的本机代码。NIO将最耗时的 I/O 操作(填充和提取缓冲区)转移回操作系统,因而可以极大的提高速度。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
I/O 与 NIO 最重要的区别就是数据打包和传输的方式,I/O 以流的方式
处理数据,而NIO 以块的方式
处理数据。
面向流的I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为 流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的I/O通常很慢。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要块的多。但是面向快的 I/O 缺少一些面向流的 I/O 所具有的简单性。
JDK 1.4 中原来的 I/O包和NIO 已经很好地集成了。
java.io.* 已经以NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.* 包中一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会很快。
通道
与 缓冲区
是NIO 中的核心对象,几乎每一个 I/O 操作都要使用它们。
通道是对原 I/O 包中流的模拟,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象
。一个Buffer
实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中。同样地,从通道中读取的任何数据都要读到缓冲区中。
通道
Channel 是一个对象,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动,而通道是双向的,可以同于读、写 或者 同时用于读写。
注意:一个流必须是 InputStream 或者 OutputStream 的子类
通道包含一下类型:
FileChannel
:从文件中读写数据DatagramChannel
:通过 UDP 读写网络中的数据SocketChannel
:通过TCP 读写网络中的数据ServerSocketChannel
:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel缓冲区
Buffer 是一个对象,它包含一些要写入或者刚读出的数据。在 NIO 中加入 Buffer对象,体现了新库与 原来I/O 的一个重要区别。
在面向流的 I/O 中,我们是将数据直接写入或者将数据直接读到 Stream 对象中。
在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读取到缓冲中的,在写入数据时,也是写入到缓冲区中的。任何时候访问 NIO 中的数据,我们都是将它放到缓冲区中的。
缓冲区实质上是一个数组。 通常它是一个字节数组,但是也可以使用其他类型的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括一下类型
对于每一种基本 Java 类型都有一种缓冲区类型:
缓冲区的状态变量
position
:当前已读写的字节数(位置)limit
:还可以读写的字节数(限制)capacity
:最大容量这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。
状态变量改变过程举例
文件NIO实例
使用 NIO 快速复制文件:
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
NIO 通常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现类 I/O 多路复用中的 Reactor模型
,一个线程Thread 使用一个选择器Selector
通过轮询的方式去监听多个通道Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听器的通道Channel为非阻塞,那么当Channel上的 I/O 事件还未到达时,就不会进入阻塞状态,而是继续轮询其它的Channel,找到 I/O事件已经到达的Channel执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件,而不是一个线程处理一件事,对于 I/O密集型的应用具有很好地性能。
创建选择器
Selector selector = Selector.open();
将通道注册到选择器中
为了接收连接,我们需要一个 ServerSocketChannel
。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。
//打开一个ServerSocketChannel
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//将ServerSocketChannel设置为非阻塞的
ssChannel.configureBlocking(false);
//将ServerSocketChannel注册到selector上
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
监听事件
int num = selector.select();
这个方法会一直阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。
获取到达的事件
//selectedKeys()方法 返回发生了事件的 SelectionKey 对象的一个集合 。
//一个SelectionKey表示一个到达的事件
Set<SelectionKey> keys = selector.selectedKeys();
//通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//对于每一个 SelectionKey, 必须确定发生的是什么 I/O 事件
//以及这个事件影响哪些 I/O 对象。
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
NIO 与普通 I/O 的区别主要有两点:
NIO 入门
NIO 总结:
NIO与传统IO的区别