Java NIO
Java NIO(New IO)是Java中可供选择的一套IO API,相比传统的IO API而言,NIO提供了不同的处理IO请求的方式。
NIO中3个核心的概念:
Java NIO: Channels and Buffers
在标准的IO API中是通过byte streams
以及character stream
来处理IO请求的,而在NIO中是通过Channeles
和Buffers
来进行处理。
数据总是从Channel
读取到Buffer
或者从Buffer
写入到Channel
。
Java NIO: Non-blocking IO
Java NIO允许你执行非阻塞IO,例如,一个线程可以要求一个channel
读取数据到buffer
中。当channel
读取数据到buffer
的时候,线程可以做别的事情。一旦数据被读到buffer
中,那么线程可以继续处理它。将数据写入通道也是如此。
Java NIO: Selectors
Java NIO中包含了selectors
这个概念。选择器是一个可以监控多个事件(比如连接打开、数据到达等等)channels
的对象。也就是说,一个单线程可以监控多个channels
中的数据
Java NIO概览
Java NIO有以下三个核心的组件组成:
- Channels
- Buffers
- Selectors
Java NIO中包含了很多的组件,但是Channel
,Buffers
和Selectors
组成了最核心的API。其余的组件,如Pipe
和FileLock
只是与三个核心组件一起使用的核心工具类。
Channels
一般来说,所有的IO在NIO中都是以Channel
(通道)。它有点像标准IO中的Stream。从管道读取数据到一个Buffer
,数据同样也可以从缓冲区被写入到Channel
中。其说明图如下:
在NIO中有几种Channel
和Buffer
类型。下面是在NIO中主要的Channel
接口:
- FileChannel: 读取数据从文件中或者到文件中
- DatagramChannel: 在网络中通过UDP协议读取或者写入数据
- SocketChannel: 在网络中通过TCP协议读取或者写入数据
- ServerSocketChannel: 允许你监听TCP连接,比如由网页服务器传送过来的连接,然后为每个进来的TCP连接创建
SocketChannel
。
这些通道覆盖了我们常用的IO请求,比如UDP+TCP网络IO请求,以及文件IO请求。
一个最基本的Channel
例子:
try {
RandomAccessFile aFile = new RandomAccessFile("/Users/lany/LearnProjects/reactLabs/GGEditor/LICENSE","rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
int byteRead = fileChannel.read(byteBuffer);
while(byteRead != -1){
System.out.println("Read"+byteRead);
byteBuffer.flip();
while(byteBuffer.hasRemaining()){
System.out.print((char)byteBuffer.get());
}
byteBuffer.clear();
byteRead = fileChannel.read(byteBuffer);
}
aFile.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
Buffers
故名思意,缓冲区,实际上是一个容器,是一个连续数组或者说是可以写入数据的内存块。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
缓冲区的基本用法
利用Buffer
进行读取或者写入数据大致情况下会遵循4个步骤:
- 写入数据到缓冲区
- 调用buffer.flip()方法
- 将数据从缓冲区中读取出来
- 调用buffer.clear()或者buffer.compact()方法
当你将数据写入到缓冲区时,缓冲区会跟踪你所写的数据量。一旦你需要读取所写入的数据,你需要将缓冲区的模式从writing
模式通过调用flip()方法切换到reading
模式。在reading
模式中缓冲区会让你读取到所有已经写入的数据。
一旦你读取完所有的数据,你需要清除缓冲区为下一次数据的写入做准备。你有两种方式实现缓冲区的清楚:调用clear()或者compact()方法。clear()方法是用来清空整个buffer里面的数据。compact()方法只是用来清除你已经读取了的数据。任何还未读取到的数据都存放在了缓冲区的开头,未来写入的数据也会放在没有读取的数据的后面。
Buffer Capacity,Position以及Limit
一个Buffer有三个参数你必须要熟知,用来好理解buffer是如何工作的:
- capacity
- position
- limit
其中position
和limit
的意义取决于buffer是read模式还是write模式。capacity
不管buffer是什么模式其意义都是一样的。下图解释了其三个参数在不同模式下的含义。
- capacity
作为内存块,一个缓冲区需要有一个确定的且可修改的长度,我们可以把它称为容量。你只能把字节、长整型、字符等写入到缓冲区,当缓冲区满后,你需要清空缓冲区然后才能继续写入更多的数据。
- position
当你写数据到缓冲区的时候,可以在某个确定的位置做一些事情。比如初始化位置为0。当一个字节或者长整型的数据被写入到缓冲区之后,缓冲区的位置会指向下一个用来插入数据的单元格。position
的最大位置为capacity
-1。
当你从缓冲区读数据的时候,同样也可以在某个确定的位置做一些事情。当你将缓冲区的模式从写模式转换成读模式的时候,position
的值会被重置为0.正如你从缓冲区的某个位置读取数据一样,该位置会预先指向像一个需要读取的数据。
- Limit
在缓冲区为写模式的时候,limit的含义是指你可以在buffer中写入多少的数据。limit
的值是跟position
的值相等的。
在缓冲区为读模式的时候,limit意味着你可以在buffer中读取数据的最大值。因此,当你将缓冲区的模式从写模式转换为读模式的时候,limit被重置到写模式中position
的值。换句话说,你可以读取到你写入的数据。
Buffer
类型:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些Buffer
接口都覆盖了Java中的基本数据类型。
Selectors
一个Selectors
(选择器)允许单线程处理多个Channel
。如果你的应用程序打开了多个连接(Channel),但是每个连接的流量却很少,那么就很方便。例如,在会话服务器中。
下面一张图说明了一个线程用一个Selector
去处理3个Channel
:
如果要使用选择器,那么就要使用它去注册通道。然后调用它的select
方法。此方法在为其中一个通道准备好事件之前一直为阻塞状态。一旦方法返回,那么线程就可以处理这些事件。
Java NIO Scatter/Gather
Java NIO具有内置的scatter/gather支持。Scatter/Gather通常用于通道的读写。
散射读取(scattering read)
是将数据从通道读入多个缓冲区的读取操作。因此,通道将来自通道的数据“分散”到多个缓冲区中。
聚合写入(gathering write)
是将数据从多个缓冲区写入到一个通道。因此,通道会将来自于多个缓冲区中的数据写入到一个通道中。
在需要单独处理传输数据的各个部分的情况下,分散/聚集非常有用。比如说,如果一个消息是由消息头和消息体组成,你也许会将请求头与请求题存放于单独的缓冲区中。这样可以使我们更容易区分请求头和请求体。
Scattering Reads
散射读取是从一个单一的通道将数据读取到多个缓冲区中。下图说明了散射读取的工作原理:
下面是散射读取的伪代码:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
通过上面代码,我们会发现首先要定义不同大小的缓冲区用来存放不同的数据,然后将缓冲区放入到缓冲区数组中,最后调用通道的read()方法将数据读取到buffer中,当buffer中的数据满了之后,就根据缓冲区数组中的顺序读取数据到下一个缓冲区。
散射读取在进入下一个缓冲区之前填充一个缓冲区的事实意味着它不适合动态大小的消息部分。换句话说,如果你有一个标题和一个正文,并且标题是固定大小(例如128个字节),那么散射读取工作正常。
Gathering Writes
聚集写入是将多个缓冲区的数据写入到一个单一的通道中。下图是聚集写入的工作原理:
下面是聚集写入的伪代码:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
缓冲区数组传递给write()方法,该方法按照在数组中遇到的顺序写入缓冲区的内容。只有存在于缓冲区的position和limit之间的数据才能被写入。也就是说,如果一个缓冲区的容量为128个字节,但是该缓冲区实际上只包含了58字节,那么只有这58个字节能被写入到通道中。因此,与散射读取相比,聚集写入与动态大小的消息部分一起工作正常。
Java NIO中通道与通道之间的传输
在Java NIO中你可以在一个通道中直接传输数据到另一个通道中。如果其中的一个通道类型为FileChannel
。FileChannel
通道中有transferTo()和transferFrom()两个方法实现该功能。
transferFrom()
该方法是将源通道的数据传输到FileChannel
中,简单的实现代码如下:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
在transferFrom()方法中有两个参数position
和count
。这两个参数告诉了channel该从源通道的哪个位置传输多少字节的数据。如果源通道的数据字节数少于给定的count字节数,那么传输的字节数将会减少。
另外,一些SocketChannel
接口或许只会传输在该接口的内置缓冲区中已经预先准备好的数据。即使该通道接口或许后面会有很多可用的数据。也就是说,当SockertChannel
传输数据到FileChannel
中的时候,并不会将SocketChannel
中的全部数据传输到FileChannel
中。
transferTo()
该方法是将文件通道中的数据传输到其他的通道中去。简单的实现代码如下:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
跟transferFrom()方法不同的是。调用的文件通道的对象不同,其余的都一样。
transferTo()方法也存在SocketChannel
的问题。SocketChannel
实现只能从FileChannel传输字节,直到发送缓冲区被填满,然后停止。