您的位置:首页 > 编程语言 > Java开发

Java NIO教程(上)

2015-11-24 21:38 357 查看
由于原文比较长,我分成了上、中、下三部分介绍,各个部分链接如下:

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有下面的核心组件构成:
Channel
Buffer
Selector
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个属性:
capacity
position
limit

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被填满才停止。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java nio