本文翻译自: http://tutorials.jenkov.com/java-nio/index.html, 本人第一次开始写博客,第一次翻译,如有问题,欢迎指正~
上一篇:JAVA NIO 概述
Java NIO Channel
Java NIO Channel 和 Java IO Stream 非常相似,不过也有一点区别:
- 我们可以同时向一个 Channel 读出和写入,但是 Stream却只能单方向的读或者写
- 我们可以异步的读写一个Channel
- Channel 总是读出数据到一个Buffer,或者从一个Buffer中写入
下面是一个图示说明,和上篇博客的图示一样,这里再次引用:
Channel的实现类
这是 Java NIO 中几个比较重要的Channel实现类:
- FileChannel -> 能够从文件中读出数据或向文件中写入数据
- DatagramChannel -> 能够使用UDP协议在网络中收发数据
- SocketChannel -> 能够使用TCP协议在网络中收发数据
- ServerSocketChannel -> 能够像Web服务器那样,监听到来的TCP连接,并为此TCP连接创建SocketChannel
Channel的简单示例代码
下面是使用FileChannel从Buffer中读入数据的简单示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
请注意调用buf.flip(),这是ByteBuffer的读和写的翻转函数。开始时,我们将数据从Channel读入到Buffer,接着我们将Buffer从写入模式翻转(flip)到读出模式,然后,我们可以将数据从Buffer中读出。本文将在下面详细介绍Java NIO Buffer。
Java NIO Buffer
当要和Java NIO Channel交互时,需要使用 Java NIO Buffer。如上文所讲,数据从Channel中读出存入Buffer,数据从Buffer读出写入Channel。
基本上来说,Buffer就是一块内存。我们可以向其中写入数据,也可以从中读出数据。这个内存块被一个提供了一系列地非常方便地操作内存块方法的NIO Java类封装了起来。
Buffer基本用法
使用一个Buffer去读写数据一般需要这4个过程:
- 向Buffer中写入数据
- 调用buffer.flip()读写模式翻转函数
- 将数据从Buffer中读出
- 调用buffer.clear()清空缓存或者调用buffer.compact()清除已读数据
当我们向Buffer写入数据时,Buffer会一直记录着写入数据的数量。一旦我们需要从Buffer中读出数据时,首先需要调用flip()函数将Buffer从写入模式翻转到读出模式,然后可以从中读出所以写入到Buffer中的数据(注意:一般flip()只用来将Buffer从写入模式翻转成读模式。当需要将Buffer从读模式翻转成写模式时,调用clear() 或 compact()。其实flip()方法的代码和clear()是一样的)。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
一旦Buffer中的所有数据,我们需要清理Buffer,为再次写入做准备。有两个函数可以实现清理操作:clear() 和 compact() 。clear()方法会清空整个Buffer内存块。而compact()方法仅清楚我们已经读出过的数据,并将未读的数据移动到内存块的起始位置,以后新写入的数据将放入到紧接在未读数据之后的位置。
下面是一个带有写、翻转、读和清除操作的简单Buffer使用示例:
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);
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();
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();
Buffer Capacity, Position and Limit(容量、位置和限制)
正如前面所讲,Buffer是一个内存块。为了完成一些操作,Buffer需要一些变量来记录这个内存块的属性。同时为了更好的理解Buffer是怎样工作的,接下来将讲解着三个属性:
- capacity 容量
- position 操作位置
- limit 操作限制位置
position 和 limit 代表的意义和Buffer是处于读模式或者写模式相关,而不管Buffer处在什么模式下,capacity 的意义不变。
下面是一张写模式和读模式下的capacity 、position 和limit 的图示说明。下文对这三个属性的讲解也是参考此图。
Capacity
Buffer管理一个内存块有固定的大小,这个大小成为“容量(Capacity)”。我们只能够向Buffer中写入和Capacity相同大小的字节,long型数或字符等。一旦Buffer写满,在想要写入新的数据前,必须清空Buffer,即把数据读出,并调用clear()或compact()方法。
Position
当向Buffer写入数据时,数据将被存入到Buffer中的内存块上的一个特定位置。在初始情况下,position值为0,即代表内存块的起始位置。当一个字节或long型数组写入到Buffer后,position将指向下一个存储单元以便以后插入数据。position最大能达到capacity - 1。
当从一个Buffer中读数据时,同样需要指定一个读的起始位置。当我们将一个Buffer从写模式翻转成读模式时,即调用flip()方法,position值复位成0。这时如果我们读一个数据,将读出position指向位置的数据,并且position会加1指向下一个存储单元。
Limit
在写模式时,limit表示我们能想Buffer中写入多少数据,此时在默认情况下limit 的值和capacity 相等。当然也可以在Buffer处于写模式中时,调用limit(int newLimit)方法,设置能写入的数据小于capacity 的限制。
当Buffer翻转到读模式时,limit 意味着我们能从缓存中读出多少数据。一次Buffer翻转到读模式时,limit 的值设为Buffer处在写模式时的position 的值。也就是说,我们最多只能读出和写入数据数量相同的数据。
常见Buffer的子类(Buffer Types)
Java NIO 有以下一种常见Buffer类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
很容易理解,这些不同的Buffer用来缓存不同的数据类型。例如,我们可以用CharBuffer缓存Char型的数据。其中MappedByteBuffer有点特殊,将在以后的章节中专门对它讲解。
分配一个缓存区(Allocating a Buffer)
想要获取一个Buffer对象,首先要为它分配内存。每个Buffer类都有一个allocate() 静态方法作为产生相应Buffer对象的工厂方法。下面是一个产生容量为48字节的ByteBuffer对象的示例代码:
ByteBuffer buf = ByteBuffer.allocate(48);
下面是一个产生容量为1024个字符的CharBuffer对象的示例代码:
CharBuffer buf = CharBuffer.allocate(1024);
向Buffer中写入数据
我们有两种方式向Buffer中写入数据:
从Channel中读出数据并写向Buffer
int bytesRead = inChannel.read(buf); //read into buffer
调用put() 方法,手动向Buffer中写入数据
buf.put(127);
put()方法有多个重载,方便我们以不同的方式向Buffer中写入数据。例如,在Buffer的特定位置写入数据,或者向Buffer中写入一整个数组的数据等。想要获取更详细的信息可以阅读具体某个类的实现文档(JavaDoc)。
从Buffer中读出数据
像写入数据一样,同样有两种方式从Buffer中读出数据:
从Buffer中读出数据并写入Channel
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);调用get()方法,手动从Buffer中读数据
byte aByte = buf.get();
get()方法同put()方法一样有多个重载,提供不同的方式从Buffer中读出数据。例如,从Buffer的特定位置读出数据,或者一次从Buffer中读出一组数据。
flip()
flip()方法将Buffer从写入模式切换到读出模式,调用flip()时,position值设定为0,limit 值设为Buffer处在写入模式时的position 的值。
换句话说,position 现在标记着读的起始位置,limit 现在标记了有多少个数据可以读。
rewind()
调用rewind()时,position 复位成0,所以此时我们可以再次读出Buffer中的所有数据。在这个方法中,并没有涉及到 limit值,因此它依然标记着Buffer中还有多少个数据可以读。
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
clear() and compact()
一旦我们完成从Buffer中读出数据,需要让Buffer 翻转到写入模式,为再次写入数据做好准备。此时我们可以调用clear() 或 compact() 完成此功能。
如果调用clear() 方法,position 复位成0, limit 值设定为capacity 的值。可以说此时Buffer被清空了,但是Buffer中的数据并没有被删除。只是我们可以从Buffer管理的内存块的起始位置开始写入数据,覆盖掉之前的数据。
如果Buffer中还有未读取的数据,调用clear()会遗弃这些未读取的数据。这意味着再也没有标记能告诉哪些数据已经读取过,哪些数据还未读取。
如果Buffer中还有未读取的数据,并且我们以后还需要读取它,只是现在有一些新的数据需要写入Buffer,此时应该调用compact(),而非clear()。
compact() 方法将Buffer中未读的数据复制到内存块的起始位置,然后将 position 指向紧靠着未读数据的最后一个元素的后面, limit 依然设为capacity 的值。现在,Buffer便可接着将新的数据写在未读数据的后面。