GuangchaoSun's Blog

NIO学习笔记

普通IO使用流的方式使用I/O。所有I/O都被视为单个的字节的移动,通过一个称为Stream的对象一次移动一个字节。流I/O用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换为对象。
NIO以块的方式处理数据。

通道和缓冲区

通道(Channel)和缓冲区(Buffer)是NIO中的核心对象,几乎在每一个I/O中都要操作他们。
通道是对原I/O包中的流的模拟。到任何目的地的所有数据都必须通过一个Channel对象。一个Buffer实质上是一个容器对象。发送给一个通道的对象都必须首先放入到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。

什么是缓冲区?

Buffer是一个对象,是固定数量的数据的容器,它包含一些要写入或者读出的对象。其作用是一个存储器,或者分段存储区,在这里数据可被存储并在之后用于检索。尽管缓冲区作用于它们存储的原始数据类型,但缓冲区十分倾向于处理字节。非字节缓冲区可以在后台执行从字节或到字节的转换,这取决于缓冲区是如何创建的。

缓冲区的工作于通道紧密联系。对于离开缓冲区的传输,都要经过通道(channel);对于传回缓冲区的传输,一个通道将数据放到您所提供的缓冲区中。

在面向流的I/O中,您将数据直接写入或者将数据读到Stream对象中。而在NIO库中,所有的数据都是用缓冲区处理的。缓冲区实质上是一个数组。

ByteBuffer的属性

  • 容量(Capacity):缓冲区能够容纳的数据元素的最大元素。这一个容量在缓冲区创建时被设定,并且永远不能被改变
  • 上界(Limit):缓冲区的第一个不能被读或者写的元素。或者说缓冲区中现存的计数。
  • 位置(Postition):下一个要被读或写的元素的索引。位置会自动由相应的get()和put()函数更新。
  • 标记(Mark):下一个要被读或者写的元素的索引。位置会自动由相应的get()和put()函数更新。

Buffer的常见方法:

  • flip():写模式转换成读模式
  • rewind():将position重置为0,一般用户重复读
  • clear():清空buffer,准备再次写入(position变为0,limit变成capacity)
  • compact():将未读取的数据拷贝到buffer的头部位
  • mark():可以标记一个位置
  • reset():可以重置到该位置

缓冲区类型

最常用的缓冲区类型是ByteBuffer。一个ByteBuffer可以在其底层字节数组上进行get/set操作
还有其他的缓冲区类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

什么是通道?

Channel是一个对象,可以通过它读取的写入数据。
正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

通道类型

通道与流的不同在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者同时用于读写。

选择器Selector

选择器提供选择执行已经就绪的任务的能力,这使得多元I/O成为可能,就绪选择和多元执行使得单线程能够有效率的同时管理多个I/O通道(channels),简单言之就是selector充当一个监视者,您需要将之前创建的一个或多个可选择的通道注册到选择器对象中。

传统的socket监控

传统的监控多个 socket 的 Java 解决方案是为每个 socket 创建一个线程并使得线程可以在 read( )调用中阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了 socket 监控器,并将 Java 虚拟机的线程调度当作了通知机制。

选择器属性

选择器(Selector)
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的通道。

可选择通道(SelectableChannel)
SelectableChannel可以被注册到Selector对象上。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。

从理论到实践

从文件中读

在NIO系统中,任何时候执行一个读操作,都是从通道中读,但不是直接从通道中读。因为所有数据最终都驻留在缓冲区中,所以是从缓冲区中读的。
因此读文件分为三个步骤:(1)从FileInputStream中获取Channel;(2)创建Buffer;(3)将数据从Channel中读到Buffer中。
代码实现:

1
2
3
4
FileInputStream fin = new FileInputStream("readme.txt");
FileChannel fc = fin.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
fc.read(buffer);//将数据从通道读到缓冲区中

写入文件

首先从FileOutputStream获取一个通道

1
2
FileOutputStream fout = new FileOutputStream("readme.txt");
FileChannel fc = fout.getChannel();

下一步是创建一个缓冲区并在其中放入一些数据

1
2
3
4
5
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i = 0; i < message.length; i++){
buffer.put(message[i]);
}
buffer.flip();

最后一步是写入缓冲区中

1
fc.write(buffer);

读写结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fcin.read(buffer);//从输入通道fcin中读入缓冲区
fcout.write(buffer);//将这些数据写入到输出通道
//检查拷贝何时完成
int r = fcin.read(buffer);
if(r == -1){
break;
}
//在从输入通道读入缓冲区之前,我们调用 clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用 flip() 方法
buffer.clear();
int r = fcin.read(buffer);
if(r == -1){
break;
}
buffer.flip();
fcout.write(buffer);

缓存区内部细节

介绍NIO中两个重要的缓冲区组件:状态变量和访问方法(accessor)。
可以用三个值指定缓冲区在任意时刻的状态

  • position:指定了下一个字节将放到数组的哪一个元素中
  • limit:表明还有多少数据需要取出(从缓存区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
  • capacity:表明可以存储在缓冲区中的最大数据容量

flip()方法:

  • 将limit设置为当前position
  • 然后将position设置为0
    clear()方法:重设缓冲区以便接收更多的字节。
  • 将limit设为与capacity相同
  • 设置position为0

缓冲区的使用:一个内部循环
下面的内部循环中概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程

1
2
3
4
5
6
7
8
9
10
while(true){
buffer.clear();
int r = fcin.read(buffer);
if (r == 1){
break;
}
buffer.flip;
fcout.write(buffer);
}

allocate():静态方法,用于分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中。

reference: