Java NIO是一套可供选择的IO API,旨在替代标准的Java IO和Java Networking API。Java NIO提供了与标准IO不同的处理方式。
Java NIO引入
1. Channels 和 Buffers(通道和缓冲区)
在标准的IO API中通常使用字符流或字节流的方式来处理数据,而在NIO中是则是使用通道(channel)和缓冲区(buffer)。数据会被从一个channel读取到一个buffer,或者从一个buffer写入到一个channel。
2. Non-blocking IO
Java NIO实现了异步非阻塞的IO。例如,一个线程可以申请一个channel来读取数据到一个buffer中。当线程从channel读取数据到buffer中时,该线程还可以做其它的事情。当数据被写入到buffer后,然后该线程可以继续对该数据进行处理。从缓冲区写入通道也类似。
3. Selectors(选择器)
Java NIO引入了selectors的概念。selector是用于监听多个channel事件(连接打开,数据到达)的对象。 因此,单个线程可以监听多个channel获取数据。
Java NIO概述
Java NIO 包括Channels、Buffers和Selectors三个核心组件。除此之外,Java NIO的类和组件还有很多,但是这三个组件构成了API的核心部分。其它的组件,如Pipe和FileLock,仅仅是通用的工具类,用来组合这三个核心组件。因此,我们的重点是这三个核心组件。
1. Channels和Buffers
基本上所有的 IO操作 在NIO 中都是从一个Channel 开始。Channel有点像流,数据可以从Channel读到Buffer中。同样,数据可以从Buffer写到Channel。
这里有几个Channel和Buffer的类型。下面是几个Java NIO中主要的Channel实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
这些channels涵盖了UDP和TCP网络IO和文件IO。
下面是几个NIO中的重要Buffer实现:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
这些Buffer涵盖了通过标准IO进行传递的基本数据类型:byte、short、int、long、float、double和char。
Java NIO也有一个MappedByteBuffer用来表示内存映射文件,后文中会介绍到。
2. Selectors
Selector允许单线程处理多个 Channel。如果你的应用程序打开了多个连接(Channel),但每个连接上的流量都很低。
例如,在一个聊天服务器中。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法将会处于阻塞状态,直到某个已经注册的channel有事件就绪。一旦方法返回,该线程就可以处理这些事件。事件的实例如:传入新的连接,数据接收等。
Java NIO 通道
Java NIO Channels和流很相似,但是有一些不同的地方:
既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
从通道读取数据到缓冲区,从缓冲区写入数据到通道。
1. Channel的实现
这里列出了在Java NIO中几个最重要的通道实现:
FileChannel:从文件中读取数据;
DatagramChannel:可以通过UDP协议读写网络数据;
SocketChannel:可以通过TCP协议读写网络数据;
ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每个新进来的连接都会创建一个SocketChannel。
2. 一个通道示例
使用FileChannel读取数据到一个Buffer中。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();//创建文件通道
ByteBuffer buf = ByteBuffer.allocate(48);//创建一个容量为48bytes的缓冲区
int bytesRead = inChannel.read(buf);//将数据从通道读入到缓冲区
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();//将buffer切换成读模式
while(buf.hasRemaining()){
System.out.print((char) buf.get());//从缓冲区获取数据
}
buf.clear();//将buffer切换成写模式
bytesRead = inChannel.read(buf);
}
aFile.close();
注意buf.flip()的调用。首先你读取数据到一个Buffer中,然后反转Buffer,接着再从Buffer中读取数据。
Java NIO缓冲区
Java NIO的Buffers通常用于和NIO Channels进行交互。众所周知,数据是从channels读入到buffers中,从buffers写出到channels中的。
缓冲区本质上是一块可以写数据,然后从中可以读取数据的内存。这块内存被包装成一个NIO Buffer对象,提供了一组方法使得方位该内存块变得很方便。
1. 缓冲区的基本用法
使用一个Buffer来读写数据一般遵循以下四个步骤:
写入数据到Buffer中;
调用buffer.flip()方法;
从Buffer读取数据;
调用buffer.clear()或buffer.compact()。
当向一个buffer中写数据时,buffer会记录下写了多少数据。当要读取数据时,需要调用flip()方法将buffer从写模式切换到读模式。在读模式中,b可以读取之前写到buffer中的所有数据。
当读完了所有的数据,需要清空这个buffer,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只是清空已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
2. 缓冲区的Capacity,Position和Limit
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
为了解Buffer的工作原理,需要熟悉的Buffer三个属性包括:
capacity
position
limit
position和limit是依赖于这个Buffer是处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
3. Capacity
作为一个内存块,Buffer有一个固定的大小值,或者叫做capacity(容量)。你只能往里写capacity个bytes,long,chars等类型的数据到buffer中。一旦buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
4. Position
当你往Buffer中写数据时,position表示当前的位置。初始的position值为0。当一个byte,long等数据写入到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。
当从一个buffer读取数据时,也是从某个特定的位置读取。当你flip一个buffer从写模式到读模式,position被重置为0。当从缓冲区的position处读取数据时,position向前移动到下一个可读的位置。
5. Limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。在写模式下limit等于buffer的capacity。
当切换到读模式,limit表示你最多能读到多少数据。因此,当将buffer切换到读模式时,limit会被设置成写模式下的position位置。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)。
6. Buffer类型
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
这些buffer类型代表了不同数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。
MappedByteBuffer 有些特别,在涉及它的专门章节中再讲。
7. 缓冲区的分配
要想获得一个Buffer对象,首先要进行分配。每个Buffer类都有一个allocate()方法。
ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);
8. 向缓冲区写数据
从Channel写到buffer。
通过Buffer的put()方法写到Buffer里。
int bytesRead = inChannel.read(buf); //read into buffer.
buf.put(127);
put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。
9. flip()
flip()方法将一个buffer从写模式切换到读模式。调用flip()方法会将position设置为0,并且设置limit为之前的position位置。
10. 从缓冲区读取数据
可以通过两种方式从Buffer中读取数据:
从buffer中读取数据到一个channel
使用get()方法从Buffer中读取数据
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();
get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。
11. Rewind
Buffer.rewind()设置position位置为0,所以你可以重新读取buffer中所有的数据。limit保持不变,仍然表示能从buffer中读取多少个元素。
12. clear() and compact()
一旦读完Buffer中的数据,需要让buffer准备好再次被写入。可以通过调用clear()或compact()方法。
如果调用clear方法,position会被设置为0,limit会被设置为capacity。换句换说,Buffer被清空了。在Buffer中的数据没有被清除。只是这些标记告诉我们可以从可以从哪里开始往buffer中写数据。
如果在buffer中还有一些未读的数据,调用clear方法将会使得这些数据被遗忘,意味着不再有任何标记哪些数据被对过,哪些还没有。
如果Buffer中仍有未读数据,而且后续还需要这些数据,但是此时想要先写一些数据,那么使用compact()方法。
compact方法会将所有未读的数据拷贝到Buffer起始处,然后将position设置为最后一个未读元素的正后面,limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
13. mark() and reset()
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
14. equals() and compareTo()
可以使用equals()和compareTo()方法比较两个Buffer。
(1)equals()
当满足下列条件时,表示两个Buffer相等:
有相同的类型(byte、char、int等)。
Buffer中剩余的byte、char等的个数相等。
Buffer中所有剩余的byte、char等都相同。
equals只是比较buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
(2) comparTo()
比较两个buffer中剩余的元素(bytes,chars等)例如排序。如果
第一个不相等的元素小于另一个Buffer中对应的元素。
所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
剩余元素是从 position到limit之间的元素。
Java NIO Scatter / Gather(分散/聚集)
Java NIO内置分散/聚集支持。分散/聚集是用于描述从通道张读取或写入到通道的操作。
分散:从channel读取是指在操作时将读取的数据写入多个buffer中。因此,channel将从channel中读取的数据分散到多buffer中。
聚集:写入channel是指在写操作时,将多个buffer的数据写入到同一个channel,因此,channel将多个buffer中的数据聚集后发送到channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
1. Scattering Reads
数据从一个channel读取到多个buffer中。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
注意,buffer首先被插入到一个数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
事实上在移动到下一个buffer之前,必须填满当前buffer,意味着它不适用于动态信息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充分散读才能正常工作。
2. Gathering Writes
聚集写是指数据从多个buffer写入到一个channel。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
buffer数组作为write方法的输入参数,顺序地将buffer中的数据写入到channel。
注意,只有在position和limit之间的数据才会被写入。因此,如果一个buffer有128字节,但是只包含了58字节的数据,那么这58字节的数据将被写入到channel中。因此聚集写操作可以很好的处理动态信息,和分散读正好相反。
Java NIO 通道之间的数据传输
在JavaNIO中,你可以直接从一个channel向另一个channel传输数据,如果其中一个channel是FileChannel,则这个FileChannel类由一个transferTo和一个transferFrom方法。
1. transferFrom
FileChannel.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);
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。
此外要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。
2. transferTo
该方法是将数据从FileChannel传输到其它的channel中。
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);
注意这个和前面的非常相似,除了调用方法的FileChannel对象不一样外,其他的都一样。
上面所说的关于SocketChannel的问题在transferTo()方法中同样存在。SocketChannel会一直传输数据直到目标buffer被填满。