这个系列的NIO主要是翻译过来的,原文地址:http://tutorials.jenkov.com/java-nio/index.html
Java NIO 教程
Java NIO:Channels and Buffers(管道和缓冲区)
在标准的IO API中,使用的是字节流和字符流。在NIO中,使用的是管道和缓冲区,数据从管道读取到缓冲区,或者从缓冲区写入到管道。
Java NIO:Non-blocking IO(非阻塞IO)
Java NIO允许能够执行非阻塞IO。例如,线程可以请求管道将数据读入缓冲区。当管道将数据读入缓冲区时,线程可以执行其他操作。一旦数据被读入缓冲区,线程就可以继续处理它。将数据写入管道也是如此。
Java NIO:Selectors(选择器)
Java NIO包含“选择器”的概念。选择器是一个对象,它可以监视事件的多个通道(例如:连接打开,数据到达等)。因此,一个线程可以监视多个数据通道。
Java NIO概述
Java NIO由以下的核心组件组成
- 管道(Channels)
- 缓冲(Buffers)
- 选择器(Selectors)
Java NIO有更多的类和组件,但在我看来,管道、缓冲区和选择器构成了API的核心。其余的组件,如Pipe和FileLock只是与三个核心组件一起使用的实用程序类。
管道和缓冲(Channels and Buffers)
通常来说,NIO中的所有IO都以管道开始。管道有点类似于流。从管道中可以将数据读入缓冲区。数据也可以从缓冲区写入管道。这里有一个例子:
有几种通道和缓冲区类型。下面是Java NIO的主要通道实现列表:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
这些通道包括UDP + TCP、网络IO、和文件IO。
Java NIO的核心缓冲实现列表:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些缓冲区涵盖了通过IO发送的基本数据类型:字节、短整型、int、长整型、浮点、双精度和字符。
Java NIO还有一个MappedByteBuffer,它与内存映射文件一起使用。
选择器(Selectors)
选择器允许单个线程处理多个管道。如果应用程序打开了许多连接(管道),但每个连接上的通信量都很低,那么这将非常方便。例如,在聊天服务器中。
下面是一个用选择器处理3个通道的线程的例子:
要使用选择器,需要给它注册管道。然后调用它的select()方法。此方法将阻塞,直到为已注册的管道之一准备好事件为止。一旦方法返回,线程就可以处理这些事件。事件的例子有传入连接、接收数据等。
Java NIO Channel(管道)
Java NIO管道类似于流,但还是有一些区别:
- 可以对一个管道读和写,而流通常是单向的(读或写)。
- 管道可以异步读写。
- 管道总是从缓冲区读取或写入数据。
如上所述,你从管道读物数据进入缓冲区,并将数据从缓冲区写入通道。这里有一个例子:
管道的实现
下面是Java NIO中最重要的管道实现类:
- FileChannel:从文件和文件中读取数据。
- DatagramChannel:可以通过UDP在网络上读写数据。
- SocketChannel:可以通过TCP在网络上读写数据。
- ServerSocketChannel:允许像web服务器一样监听传入的TCP连接。为每个传入的连接创建一个SocketChannel。
管道基本的使用例子
下面是一个使用FileChannel将一些数据读入缓冲区的基本示例:
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()的调用。首先读入缓冲区。翻转,然后读出。
Java NIO Buffer
Java NIO缓冲区经常用在与NIO管道交互中。数据从管道读取到缓冲区,然后从缓冲区写入到管道。
缓冲区本质上是一块内存,可以将写入数据,然后再读取数据。这个内存块被包装在NIO缓冲区对象中,该对象提供了一组方法,使使用内存块变得更容易。
缓冲区的基本使用
使用缓冲区读取和写入数据通常遵循以下4个步骤:
- 将数据写入缓冲区
- 调用buffer.flip()
- 从缓冲区读取数据
- 调用buffer.clear()或buffer.compact()
当数据写入缓冲区时,缓冲区会跟踪写入了多少数据。一旦需要读取数据,就需要调用flip()方法调用将缓冲区从写入模式切换到读取模式。在读取模式下,缓冲区允许读取写入缓冲区的所有数据。
一旦读取了所有数据,就需要把缓冲区清空,为再次写入做准备。可以通过两种方式实现清除缓冲区:调用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();
缓冲容量,位置和极限
缓冲区本质上是一块内存,可以将数据写入其中,然后再读取数据。这个内存块被包装在NIO缓冲区对象中,该对象提供了一组方法,使使用内存块变得更容易。
为了理解缓冲区的工作方式,缓冲区的三个属性需要熟悉。这些都是:
- capacity(容量)
- position(位置)
- limit(极限)
位置和极限的含义取决于缓冲区是读模式还是写模式。无论缓冲模式如何,容量总是相同的。
这是一个在写和读模式的容量,位置和限制的插图。
- Capacity
缓冲区作为内存块,具有一定的固定大小,也称为“容量”。你只能在缓冲区写入:容量字节、长字节、字符等。一旦缓冲区满了,需要清空它(读取数据或者清除数据),然后才能向缓冲区写入更多数据。
- Position
当你往缓冲区写入数据时,会在一个确定的position写入。初始position是0。当一个字节、长等被写入缓冲区时,position将被提前指向缓冲区中的下一个单元格,以便插入数据。position可以最大变成容量- 1。
从缓冲区读取数据时,也要从给定的position读取数据。当缓冲区从写入模式翻转到读取模式时,position将重置为0。从缓冲区读取数据时,其实是从position读取数据,position将被提前到下一个要读取的position。
- Limit
在写模式中,缓冲区的极限是从缓冲区中可以写入的数据的最大限制。在写模式中,极限等于缓冲区的容量。
将缓冲区翻转到读模式时,limit是指可以从数据读取的数据量的限制。因此,当将缓冲区翻转到读模式时,极限设置为写模式的写入position。换句话说,你可以读取任意多的字节(极限设置为写入的字节数,以position标记)。
缓冲类型
Java NIO附带了以下缓冲区类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些缓冲区类型表示不同的数据类型。也就是说允许将缓冲区中的字节处理为char、short、int、long、float或double。
分配一个缓冲区
要获得缓冲区对象,必须首先分配它。每个缓冲区类都有一个allocate ()方法来完成此任务。下面的例子显示了ByteBuffer的分配,容量为48字节:
ByteBuffer buf = ByteBuffer. allocation (48);
下面是一个为1024个字符分配空间的CharBuffer的例子:
CharBuffer buf = CharBuffer. allocation (1024);
将数据写入缓冲区
有两种方式将数据写入缓冲区:
- 将数据从管道写入缓冲区
- 通过缓冲区的put()方法,自己将数据写入缓冲区。
下面是一个显示通道如何将数据写入缓冲区的例子:
int bytesRead = inChannel.read(buf); //read into buffer.
下面是一个通过put()方法将数据写入缓冲区的例子:
buf.put(127);
put()方法还有许多其他版本,允许以许多不同的方式将数据写入缓冲区。例如,在特定位置写入,或将字节数组写入缓冲区。有关具体缓冲区实现的详细信息,请参阅JavaDoc。
flip()
flip()方法的作用是:将缓冲区从写入模式切换到读取模式。调用flip()将position 设置回0,并将极限为刚才的位置。
换句话说,position现在标记了读取位置,limit标记了写入缓冲区的字节数、字符数等——可以读取的字节数、字符数等的限制。
从缓冲区读取数据
从缓冲区读取数据有两种方式。
- 从缓冲区读取数据到管道。
- 使用get()方法之一从缓冲区读取数据。
下面是如何将数据从缓冲区读取到通道的示例:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
下面是一个使用get()方法从缓冲区读取数据的例子:
byte aByte = buf.get();
get()方法还有许多其他版本,允许以许多不同的方式从缓冲区读取数据。例如,在特定位置读取,或者从缓冲区读取字节数组。有关具体缓冲区实现的详细信息,请参阅JavaDoc。
rewind()
rewind()将位置设置为0,这样就可以重新读取缓冲区中的所有数据。极限保持不变,因此仍然标记有多少元素(字节、字符等)可以从缓冲区读取。
clear() and compact()
- 从缓冲区读取数据之后,必须让缓冲区为再次写入做好准备。可以通过调用clear()或调用compact()来做到这一点。
- 如果调用clear(),该位置将被设置为0,并限制容量。换句话说,缓冲区被清除。缓冲区中的数据没有清除。只有标记告诉你可以将数据写入缓冲区的位置。
- 当你调用clear()时,如果缓冲区中有任何未读数据,则数据将被“遗忘”,意味着不再有任何标记说明哪些数据已被读取,哪些数据未被读取。
- 如果缓冲区中仍然有未读数据,并且你希望稍后读取它,但是你需要先写一些东西,那么调用compact()而不是clear()。
- compact()将所有未读的数据复制到缓冲区的开头。然后将position设置为最后一个未读元素之后的位置。与clear()一样,limit属性仍然设置为容量。现在缓冲区可以写入了,但是不会覆盖未读数据。
mark() and reset()
可以通过调用Buffer.mark()方法在缓冲区中标记给定的位置。然后,您可以通过调用Buffer.reset()方法将位置重置回标记位置。下面是一个例子:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals()和compareTo()
可以使用equals()和compareTo()比较两个缓冲区。
equals()
如果两个缓冲区相等:
- 它们的类型相同(字节、字符、int等)。
- 它们在缓冲区中有相同数量的剩余字节、字符等。
- 所有剩余的字节、字符等都是相等的。
equals()只比较缓冲区的一部分,而不是缓冲区中的每个元素。实际上,它只是比较缓冲区中的其余元素。
compareTo()
compareTo()方法比较了两个缓冲区的其余元素(字节、字符等),以用于排序例程。如果:
- 第一个元素等于另一个缓冲区中的相应元素,比另一个缓冲区中的元素小。
- 所有元素都是相等的,但是第一个缓冲区在第二个缓冲区之前耗尽了元素(它的元素更少)。