参考:http://tutorials.jenkov.com/java-nio/index.html
1、Java NIO Tutorial
NIO最早出现在Java 1.4版本中,从那个时候开始,Java至少有两套可用的IO方面的API集,一套是标准的,另一套就是NIO,两者的工作原理不同。
Java NIO: Channels and Buffers
在标准IO系统中,用户直接与字节流或者字符流打交道。在NIO中用户总是与channel与buffer打交道,读操作意味着数据从channel读入到buffer,而写操作则意味着将buffer中的数据写入到channel。
Java NIO: Non-blocking IO
NIO能够使IO操作以非阻塞的方式工作。例如,线程可以请求channel读数据到buffer,在channel读数据到buffer的过程中,线程可以去做其它的事情,一旦数据被读入进buffer,线程可以回过头来处理buffer中的数据,将buffer中的数据写入到channel与此过程类似。
Java NIO: Selectors
NIO包含“选择器”的概念,本质上它是一个channel的多路复用器,能够监视注册在其上的channel状态,如连接打开,或者连接上有数据到达,channel处于可读状态,或者内核写缓存有剩余空间,channel处于可写状态等。这样的话,单个线程可以处理多路channel的读写,低层应该是利用操作系统提供的内核函数实现,如Linux中的selector、poll、epoll等。
本节关于NIO的介绍比较笼统,不必深究,容易把自己搞迷糊,只需要知道大概概念就可以,详细的实现原理后边的章节会介绍。
2、Java NIO Channel
Channel是Java NIO的核心概念之一,类似标准IO中的stream,但存在几个不同的点,这些很关键,如下:
- Channel可同时读、写,而stream是单向的,要不就只读,要不就只写。
- Channel可异步读写(原文:Channels can be read and written asynchronously),我觉得这句话不准确,应该是Channel可非阻塞读写,非阻塞与异步是不同的概念。
- Channel总是通过buffer进行读写操作。读的时候将数据从数据源写入buffer,写的时候将数据从buffer写入channel。
注意channel与stream的相同点与不同点,这很重要。
Channel Implementations
Channel是一个抽象的概念,它有如下几种具体的实现:
- FileChannel,文件。
- DatagramChannel,UDP。
- SocketChannel,TCP客户端。
- ServerSocketChannel,TCP服务端。
Basic Channel Example
下边的代码虽然简单,但基本说明了channel的工作原理,需要仔细解读,我添加了一些注释。
// 打开文件,注意使用的是RandomAccessFile类
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
// 取得FileChannel,这说明了FileChannel是如何创建出来的。
FileChannel inChannel = aFile.getChannel();
// 分配字节类型的缓存,长度是48。显然增长缓存长度可降低读写次数,但过长的话可能浪费空间。
ByteBuffer buf = ByteBuffer.allocate(48);
// 下边这行代码很关键,涉及到channel,channel低层的文件,以及刚刚分配好的buffer。
// 要求channel从文件读数据进来,并且写入到buffer中,buffer最大48个字节。
// 另外read操作不会阻塞,不管是否能读到数据,它都立即返回。
// 返回值代表读到的字节数,如果为-1表示文件结尾,它有可能是0,但不会超过48。
int bytesRead = inChannel.read(buf);
// 文件全部读完后,循环退出。这里体现出非阻塞的好处,不管有没有数据,不管是否有48个字节的数据,读操作立刻返回,线程可以做其它的事情。
while (bytesRead != -1) {
// 输出读到的字节数
System.out.println("Read " + bytesRead);
// 当调用channel的read方法后,buffer处于被channel写入的状态,处于写模式。
// 调用下边这句话后,buffer就要切换状态到读模式,此时用户可从buffer中取出数据。
buf.flip();
// 将buffer中的数据按字节全部取出
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
// 清空buffer中的数据
buf.clear();
// 继续让channel从文件读数据并写入buffer。
bytesRead = inChannel.read(buf);
}
// 关闭文件
aFile.close();
3、Java NIO Buffer
Buffer在NIO中是另一个极其重要的概念,非常关键。在NIO模式下,用户主要与channel与buffer打交道,相对来说与buffer打交道的机会更多一些。Channel相当于是一个双向通道,将后端的数据源如文件、socket等与buffer连接起来。一旦通道建立完成,其它时间则主要与buffer打交道,此时可以把buffer看成是数据源的代理或者是用户与数据源之间的中间商,操作buffer就相当于间接操作数据源,并且操作是非阻塞的。
Buffer首先是内存中的一块存储空间,当然它不可能只是一块单纯的内存,为了协调数据源与用户,NIO中的buffer封装了一些特有的成员与方法。
Basic Buffer Usage
使用buffer读写数据,无论是读还是写都涉及四个具体的小步骤,下边分别详细说明。
读操作:
- 向buffer写入数据。这一步通过调用channel的read()方法实现,例如本文第二节中出现的代码:bytesRead = inChannel.read(buf);,要求channel从它后端的数据源中read出数据并写入buffer,注意channel的非阻塞特性,此时buffer处于写模式。
- 调用buffer的flip()方法,实现写模式到读模式的转换,表示用户接下来将从buffer中读数据。
- 用户从buffer中将数据读出,例如调用buffer的get()方法等。
- 调用buffer的clear()方法,彻底清空buffer中的数据,既使buffer中存在着用户尚未读出来的数据。或者compact()方法,只清空已经读出来的数据,未读出的数据则继续保留并移动到buffer的开始处。假如我们还需要继续从文件读数据,则继续调用channel的read()方法,并循环以上过程,只到在某个条件满足退出循环。
写操作:
- 用户向buffer中写入或者追加数据,此时buffer处于写模式。
- 调用buffer的flip()方法,实现写模式到读模式的转换,表示接下来会要求channel将buffer中数据读出并写入到后端的数据源。
- 从buffer中读出数据。这一步通过调用channel的write()方法实现,这要求channel从buffer中读出数据并写入到后端的数据源,注意channel的非阻塞特性。
- 用户判断buffer的状态,确认写入的进度,如buffer中的数据有多少已经被channel写入到后端,有多少尚未写入。用户可持续这四个步骤,直到写入全部数据。
读操作的简单代码可参考第二节。
Buffer Capacity, Position and Limit
要理解buffer如何工作,需要熟悉它的三个属性。
- capacity
- position
- limit
Capacity表示buffer的容量,容量在创建buffer时指定,它是一成不变的。通过前边的介绍可知,buffer有模式,通过flip()方法可在读、写模式之间切换。模式不同,则position与limit的含义也不同。先看一张图,稍后解释它。
看一下上图中右边的图,它表示写模式下的buffer。此时,postion代表下一个可以写入的位置,最开始时这个值是0,随着数据的增加position会持续变大。而limit此时与capacity相同,表示position的最大值限制,postion不能超过limit的限制。
将buffer由写模式切换到读模式后,postion与limit的含义及值均发生变化。postion指向buffer开始的位置,也就是0。而limit则指向写模式下的postion的位置。随着读操作的进行,postion的值增加,但它一定小于limit的值,因为limit及其以后的空间是无效的,还没有写入数据。
假如再将buffer由读模式切换到写模式,有三种可能。
- 如果在读模式下buffer中的数据即没有通过clear()方法彻底清空,也没有通过compact()方法将已经读取的数据清空,则buffer切换回写模式后什么都不会变,相当于没有切换。
- 如果在读模式下buffer中的数据通过clear()方法彻底清空,则切换回写模式后,postion将指向0的位置,相当于这是一个空的buffer。
- 如果在读模式下buffer中的数据通过调用compact()方法,只是将已经读取过的数据清空。则切换回写模式后,buffer中剩余的未读数据将会向buffer的顶端移动,同时postion的位置也会向顶端方向移动,也就是未读数据仍然保留。
Buffer Types
Buffer的具体实现有几下几种:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Allocating a Buffer
ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);
Writing Data to a Buffer
在读操作时,由channel将后端数据写入到buffer,在写操作时,由用户将数据写入到buffer。
读操作下channel向buffer写入数据:
int bytesRead = inChannel.read(buf); //read into buffer.
这里向buffer中写入数据,但channel的方法却是read(),有点奇怪。对于channel的read()方法而言,更细致的说法是read then write,先从数据源读,然后再写入buffer。
用户向buffer与入数据:
buf.put(127);
buffer的put()方法有很多版本,用户可以以多种方式向buffer中写入数据,如果需要可查询Java提供的随机文档。
flip()方法
调用此方法后,如果buffer处于写模式则切换到读模式,反之亦然,它内部的实现原理在介绍buffer如何工作时已经说明。
Reading Data from a Buffer
写操作下channel从buffer读数据:
//read from buffer