由于原文比较长,我分成了上、中、下三部分介绍,各个部分链接如下:
Java NIO 教程(上)
Java NIO 教程(中)
Java NIO 教程(下)
原文:Java NIO Tutorial,作者:Jakob Jenkov,译文版本:version 1.0
1.Java NIO指南
Java NIO(New IO)是Java1.4引入的一种新IO,可以代替标准Java IO和Java Networking的API。Java NIO提供了一种和标准IO的API不同的IO工作方式。
1.1Java NIO:Channel和Buffer
在标准IO的API中,你可以使用字节流和字符流。在NIO中,你将使用通道(Channel)和缓冲区(Buffer)。数据总是Channel从读入Buffer,或者从Buffer写入Channel。
1.2Java NIO:非阻塞IO(Non-blocking IO)
Java NIO是你能处理非阻塞IO。例如,一个线程可以让一个Channel读入数据到Buffer。当Channel往Buffer中读入数据时,这个线程可以做其他的事情。一旦数据被读入缓冲区,这个线程就能继续处理它。这同样适用于将数据写入到Channel。
1.3Java NIO:选择器(Selector)
Java NIO包含选择器(Selector)的概念。Selector是一个监控多个Channel事件的对象(如,链接开启,数据到达等)。因此,单个线程可以监控多个Channel数据。
2.Java NIO概述
Java NIO有下面的核心组件构成:
Java NIO有很很多的类和组件,但是在我看来,Channel、Buffer和Selector是NIO API的核心。像Pipe和FileLock等其他组件是和这三个核心组件结合使用的工具类。因此,在NIO概述中我将关注这三个组件。其他组件将在本教程的相应章节进行解释。
2.1Channel和Buffer
在NIO中,所有的IO都起始于Channel。Channel有一点儿像流(Stream)。来自Channel的数据可以读入Buffer。数据也可以从Buffer写入到Channel。这有一副图解释:
Channel和Buffer有好几种类型。下面是Java NIO中一些主要Channel实现的列表:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
如你所见,这些Channel包含了UDP+TCP网络IO和文件IO。
与这些类一起的有些有趣的接口,但为了简单起见,我将不在概述中介绍它们。本教程其他相关章节将会对它们进行解释。
下面是Java NIO中核心Buffer实现的列表:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些Buffer包含了你能通过IO发送的基本数据类型:byte、short、int、long、float、double、和characters。
Java NIO还有一个与内存映射文件一起使用的MappedByteBuffer。我将不会在概述中讲它。
2.2Selector
Selector允许单个线程控制多个Channel。如果你的应用打开了很多连接(Channel),但是每个连接都是低流量的,那么用Selector就很方便。比如,在一个聊天服务器中。
下面是用一个线程使用一个Selector控制3个Channel的示意图:
要使用Selector,你需要向Selector注册Channel。然后,你可以调用它的 select() 方法。这个方法将会一直阻塞,直到注册的Channel中的其中之一有事件准备就绪。一旦该方法返回,这个线程就可以处理这些事件。事件有如新来的连接,数据接收等。
3.Java NIO之 Channel
Java NIO Channel和流(Stream)相似,以下是几个不同点:
你可以同时从Channel中读数据和向Channel中写数据。Stream的读/写通常是单向的。
Channel可以异步地读和写。
Channel总是读数据到一个Buffer,或从一个Buffer中写入数据。
正如上面提到的,你从Channel中读数据到Buffer,从Buffer中写数据到Channel。下面是示意图:
3.1Channel的实现
在Java NIO中下面是最重要的Channel实现:
- FileChannel:从文件中读写数据。
- DatagramChannel:通过UDP读写网络中的数据。
- SocketChannel:通过TCP读写网络中的数据。
- ServerSocketChannel:让你像Web服务器那样监听来自TCP的连接,并为每一个新来的连接创建一个SocketChannel。
3.2简单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()的调用。首先你读数据到Buffer,然后你flip(译者注:姑且理解为更新)它,接着在从Buffer中取出数据。
4.Java NIO之 Buffer
Java NIO Buffer被用于和NIO Channel交互。如你所知,数据从Channel读入到Buffer,和从Buffer中写入到Channel。
Buffer本质上是一块可以写入数据,然后读出数据的内存。这个内存块被包装成NIO Buffer对象,它提供了一套方法,用于方便地访问这个内存块。
4.1Buffer的基本使用
通常使用Buffer读写数据都分以下4个步骤处理:
- 写入数据到Buffer
- 调用buffer.flip()
- 从Buffer读出数据
- 调用buffer.clear()或buffer.compact()
当你向Buffer中写数据时,Buffer会记录你写了多少数据。一旦你需要读数据,你需要调用flip()方法把Buffer从写模式转换为读模式。在读模式下,可以读取所有之前写入Buffer中的数据。
一旦读完数据,你需要清空Buffer,使它为下次写入数据做准备。有两种方式清空Buffer:调用clear()或者调用compact()。clear()方法清空整个Buffer。compact()方法仅仅清空已经读过的数据。其他没有读过的数据移到Buffer起始位置,新写入的数据放在Buffer中未读过的数据后面。
下面是一个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);
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();
4.2Buffer的Capacity、Position和Limit
Buffer本质上是一块可以写入数据,然后读出数据的内存。这个内存块被包装成NIO Buffer对象,它提供了一套方法,用于方便地访问这个内存块。
为了理解Buffer怎么工作的,你需要熟悉Buffer的3个属性:
position和limit的含义取决于Buffer是读模式还是写模式。不管Buffer处于什么模式,capacity总是不变的。
下面是capacity、position和limit在读模式和写模式中的示意图。具体的解释在插图之后。
4.2.1Capacity
作为一个内存块,Buffer有一个固定大小,被称为“Capacity”。你只可以往Buffer中写入Capacity数量的byte、long、 char等。一旦Buffer满了,在往Buffer里写入更多数据前,你需要清空它(读出数据,或者清除数据)。
4.2.2Position
当你写数据到Buffer时,此时的位置就是Position。初始化的Position值是0。当byte、long等类型被写入到Buffer时,Position会移动的下一个可插入数据的Buffer单元。Position最大值是Capacity-1。
当你从Buffer读数据时,是从一个给定的Position开始。当你把Buffer从写模式转换为读模式时,这个Position值被重置为0。当从Buffer中读数据时,Position会移动到下一个可读的位置。
4.2.3Limit
在写模式下,Buffer的Limit是限制你可以写入多少数据到Buffer中。写模式下,Limit值等于Buffer的Capacity值。
当转换Buffer到读模式,Limit意味着你可以从Buffer中读出多少数据。因此,Buffer转换到读模式时,Limit值被设置为写模式下的Position值。换句话说,你写多少字节,就可以读多少字节(Limit值被设置为写的数据量,即写模式下的Position值)。
4.3Buffer类型
Java NIO中有以下几种Buffer类型:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- MappedByteBuffer
如你所见,这些Buffer类型代表不同的数据类型。换句话说,你可以通过char、short、int、long、float或者double操作Buffer中的字节。
MappedByteBuffer有点儿特殊,将在专门的章节讲解。
4.4Buffer的分配
为了获得Buffer对象,你必须首先分配它。每一个Buffer类都有一个allocate()方法。下面是ByteBuffer分配48个字节的例子:
ByteBuffer buf = ByteBuffer.allocate(48);
下面是CharBuffer分配1024个字符的例子:
CharBuffer buf = CharBuffer.allocate(1024);
4.5写数据到Buffer
有两种方式往Buffer里写数据:
- 从Channel中写数据到Buffer
- 通过Buffer的put()方法,写数据到Buffer
下面是从Channel写数据到Buffer中的例子:
int bytesRead = inChannel.read(buf) //read into buffer.
下面是通过put()方法写数据到Buffer中的例子:
buf.put(127);
有很多版本的put()方法,允许你以不同的方式写数据到Buffer中。例如,在指定的位置写,或者写一个byte类型的数组写到Buffer中。更多细节请参考JavaDoc中Buffer的具体实现。
4.6flip()
flip()方法用于把Buffer从写模式转换到读模式。调用flip()方法把Position值设为0,并把Limit值设为之前Position值。
换句话说,Position值标记开始读的位置,Limit值标记之前写入了多少byte、char等,即多少byte、char等可以读。
4.7从Buffer读数据
下面是从Buffer读数据的两种方式:
- 从Buffer读数据到Channel中
- 使用get()方法从Buffer中读数据
下面从Buffer读数据到Channel的例子:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
下面是使用get()方法从Buffer中读数据的例子:
byte aByte = buf.get();
有很多版本的get()方法,运行你以不同的方式从Buffer中读数据。例如,读指定位置的数据,或者从Buffer中读一个byte类型的数组。更多细节请参考JavaDoc中Buffer的具体实现。
4.8rewind()
Buffer.rewind()设置Position值为0,所以你可以重新读取Buffer中的所有数据。Limit保持不变,仍然表示可以从Buffer中读取多少个元素(byte、char等)。
4.9clear()和compact()
一旦从Buffer中读完数据,你必须让Buffer可以被再次写入。你可以通过调用clear()或者compact()做到。
如果调用clear()方法Position值被设为0,并且Limit值设为Capacity值。换句话说,Buffer被清空了。但Buffer中的数据没有被清除。只是这些标记告诉你可以从哪些地方往Buffer中写数据。
如果当你调用clear()时,在Buffer中有一些还没有读的数据,那么这些数据将“被遗忘”,意味着不再有任何标记告诉你哪些数据已经渡过,哪些数据还有没。
如果Buffer中一直有数据没读,而且你想以后读取,但是你又需要先写入一些数据,那么可以调用compact()代替clear()。
compact()方法拷贝所有未读过的数据到Buffer的起始位置。然后Position被设置到最后一个未读元素的后面。Limit值一直设为Capacity值,像clear()一样。现在Buffer可以写入了,但是你不能覆盖未读过的数据。
4.10mark()和reset()
通过调用Buffer.mark()方法,你可以在Buffer中标记一个指定的位置。之后通过调用Buffer.reset()方法,你可以把这个位置重新设置为标记的位置。下面是一个例子:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
4.11equals()和compareTo()
使用equals()和compareTo()可以比较两个Buffer。
4.11.1equals()
当满足下列条件是,两个Buffer相等:
- 它们有相同的类型(byte、char、int等)
- 在Buffer中,剩余的byte、char等数量相同
- 所有剩余的byte、char等值相等
如你所见,equals()仅仅比较Buffer的一部分,而不是Buffer中每一个元素。事实上,它只比较Buffer中剩余的元素。
4.11.2compareTo()
compareTo()方法比较两个Buffer中剩余的元素(byte、char等),如果满足下列条件,则认为一个Buffer比两一个Buffer小:
- 与另一个Buffer相应元素相等的第一个元素,小于另一个Buffer
- 所有元素都相等,但是第一个Buffer比第二个Buffer用完所有元素(即第一个Buffer有更少的元素个数)。
5.Java NIO之Scatter/Gather
Java NIO带有内置的Scatter/Gather。Scatter/Gather是用于从Channel读取和向Channel写入的概念。
Scatter:从Channel中分散读(Scatter read)是向多个Buffer中读数据的读操作。因此,Scatter将来自Channel的数据读入到多个Buffer中。
Gather:收集写(Gather write)入Channel是将多个Buffer的数据写入到一个Channel的写操作。因此,Gather将多个Buffer的数据写入到一个Channel。
Scatter/Gather在你需要将几个部分的数据分开传输的情况下是很有用的。例如,如果一个消息由消息头和消息体组成,那么你可能需要将消息头和消息体分别放到不同Buffer中。这样你可以方便的分开处理消息头和消息体。
5.1分散读(Scattering Reads)
Scattering read是指数据从一个Channel中读入到多个Buffer中。下面是示意图:
下面Scattering read的示例代码:
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中写。
Scatter在移动到下一个Buffer前,必须要填满当前的Buffer,这意味着它不适用于大小不固定的动态消息。换句话说,如果有一个消息头和一个消息体,并且消息头是固定大小的(如,128byte),然后Scatter才能可以工作的很好。
5.2收集写(Gathering Writes)
Gathering write是指将多个Buffer中的数据写入到一个Channel。下面是示意图:
下面是Gathering write的示例代码:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
Buffer的数组是write()方法的参数,write()方法按数组中Buffer的顺序将Buffer的内容写入到Channel中。只有Buffer中Position和Limit之间的数据是可以写的。因此,如果一个128byte的Buffer,却只包含58byte的数据,那么仅仅这58byte的数据可以写入到Channel。所以,Gather对于大小变化的动态数据可以工作的很好,和Scatter恰恰相反。
6.Java NIO之Channel之间的传输
在Java NIO中,如果两个Channel中有一个是FileChannel,那么你可以直接将数据传给另一个Channel。FileChannel类中有一个transferTo()和一个transferFrom()帮你做到这点。
6.1transferFrom()
FileChannel.transferFrom()方法将数据从一个源Channel传给这个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和count表示从position位置开始,最多传给count字节的数据到目标文件。如果源Channel的空间小于count字节,那么所传输的字节数要小于请求的字节数。
另外,在一些SocketChannel的实现中,SocketChannel只传输此刻准备好的数据(可能不足count字节)。因此,transferFrom()不会将请求的所有数据(count字节的数据)全部从SocketChannel传给FileChannel。
6.2transferTo()
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对象不同外,其他都一样。
transferTo()方法中同样有SocketChannel的问题。SocketChannel的实现会一直传输数据直到目标Buffer被填满才停止。