2011年6月25日

NIO 分散 聚集

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。

分散/聚集 I/O 对于将数据流划分为单独的部分很有用,这有助于实现复杂的数据格式。

分散/聚集 I/O

通道可以有选择地实现两个新的接口: ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:

  • long read( ByteBuffer[] dsts );
  • long read( ByteBuffer[] dsts, int offset, int length );

这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区数组。

在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

分散/聚集的应用

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据,所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

聚集写入

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区数组的方法:

  • long write( ByteBuffer[] srcs );
  • long write( ByteBuffer[] srcs, int offset, int length );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致,您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流,以便跨越网络传输消息。

使用示例代码:

RandomAccessFile file = new RandomAccessFile("testFile.txt","rw");  FileChannel channel = file.getChannel();  Scatter: ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body   = ByteBuffer.allocate(1024);  ByteBuffer[] bufferArray = { header, body };  channel.read(buffers);  Gather: ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body   = ByteBuffer.allocate(1024);  //write data into buffers  ByteBuffer[] bufferArray = { header, body };  channel.write(buffers);

--
we drink green tea

如何讲文件映射到内存

将文件映射到内存

了解内存映射的最好方法是使用例子。在下面的例子中,我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:

 MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,      0, 1024 ); 

map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。


--
we drink green tea

nio 缓冲区 操作

关于缓冲区的更多内容

概述

到目前为止,您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类,在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。

本节将讨论使用缓冲区的一些更复杂的方面,比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何创建不同类型的缓冲区以达到不同的目的,如可保护数据不被修改的 只读 缓冲区,和直接映射到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中创建内存映射文件。

缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区。要创建缓冲区,您必须 分配 它。我们使用静态方法 allocate() 来分配缓冲区:

 ByteBuffer buffer = ByteBuffer.allocate( 1024 ); 

allocate() 方法分配一个具有指定大小的底层数组,并将它包装到一个缓冲区对象中 ― 在本例中是一个 ByteBuffer

您还可以将一个现有的数组转换为缓冲区,如下所示:

 byte array[] = new byte[1024]; ByteBuffer buffer = ByteBuffer.wrap( array ); 

本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层数据就可以通过缓冲区或者直接访问。

缓冲区分片

slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的 ByteBuffer

 ByteBuffer buffer = ByteBuffer.allocate( 10 ); 

然后使用数据来填充这个缓冲区,在第 n 个槽中放入数字 n

 for (int i=0; i<buffer.capacity(); ++i) {      buffer.put( (byte)i ); } 

现在我们对这个缓冲区 分片 ,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口 

窗口的起始和结束位置通过设置 position 和 limit 值来指定,然后调用 Buffer 的 slice() 方法:

 buffer.position( 3 ); buffer.limit( 7 ); ByteBuffer slice = buffer.slice(); 

片 是缓冲区的 子缓冲区 。不过, 片段 和 缓冲区 共享同一个底层数据数组,我们在下一节将会看到这一点。

缓冲区份片和数据共享

我们已经创建了原缓冲区的子缓冲区,并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。

我们遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。

 for (int i=0; i<slice.capacity(); ++i) {      byte b = slice.get( i );      b *= 11;      slice.put( i, b ); } 

最后,再看一下原缓冲区中的内容:

 buffer.position( 0 ); buffer.limit( buffer.capacity() );  while (buffer.remaining()>0) {      System.out.println( buffer.get() ); } 

结果表明只有在子缓冲区窗口中的元素被改变了:

 $ java SliceBuffer 0 1 2 33 44 55 66 7 8 9 

缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

只读缓冲区

只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据),只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。


--
we drink green tea

bytebuffer 的get和set 方法

访问方法

到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。

或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。

在本节的最后,我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。

get() 方法

ByteBuffer 类中有四个 get() 方法:

  1. byte get();
  2. ByteBuffer get( byte dst[] );
  3. ByteBuffer get( byte dst[], int offset, int length );
  4. byte get( int index );

第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。

此外,我们认为前三个 get() 方法是相对的,而最后一个方法是绝对的。 相对 意味着 get() 操作服从 limit 和 position 值 ― 更明确地说,字节是从当前 position 读取的,而 position 在 get 之后会增加。另一方面,一个 绝对 方法会忽略 limit 和 position 值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。

上面列出的方法对应于 ByteBuffer 类。其他类有等价的 get() 方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。

put()方法

ByteBuffer 类中有五个 put() 方法:

  1. ByteBuffer put( byte b );
  2. ByteBuffer put( byte src[] );
  3. ByteBuffer put( byte src[], int offset, int length );
  4. ByteBuffer put( ByteBuffer src );
  5. ByteBuffer put( int index, byte b );

第一个方法 写入(put) 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。

与 get() 方法一样,我们将把 put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的,而第五个方法是绝对的。

上面显示的方法对应于 ByteBuffer 类。其他类有等价的 put() 方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。

类型化的 get() 和 put() 方法

除了前些小节中描述的 get() 和 put() 方法, ByteBuffer 还有用于读写不同类型的值的其他方法,如下所示:

  • getByte()
  • getChar()
  • getShort()
  • getInt()
  • getLong()
  • getFloat()
  • getDouble()
  • putByte()
  • putChar()
  • putShort()
  • putInt()
  • putLong()
  • putFloat()
  • putDouble()

事实上,这其中的每个方法都有两种类型 ― 一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。

您可以在例子程序 TypesInByteBuffer.java 中看到这些方法的实际应用。


--
we drink green tea

sample code for nio java turial

import java.io.FileInputStream;
 2import java.io.IOException;
 3import java.nio.ByteBuffer;
 4import java.nio.channels.FileChannel;
 5
 6public class Test {           
 7    /**  
 8     * 使用IO读取指定文件的前1024个字节的内容。  
 9     * @param file 指定文件名称。  
10     * @throws java.io.IOException IO异常。  
11     */
 
12    public static void ioRead(String file) throws IOException{
13        FileInputStream in = new FileInputStream(file);
14        byte[] b = new byte[1024];
15        in.read(b);
16        System.out.println(new String(b));
17        in.close();
18    }

19
20    /**  
21     * 使用NIO读取指定文件的前1024个字节的内容。  
22     * @param file 指定文件名称。  
23     * @throws java.io.IOException IO异常。  
24     */
 
25    public static void nioRead(String file) throws IOException{
26        FileInputStream in = new FileInputStream(file);
27        FileChannel channel = in.getChannel();
28
29        ByteBuffer buffer = ByteBuffer.allocate(1024);
30        channel.read(buffer);
31        byte[] b = buffer.array();
32        System.out.println(new String(b));
33        channel.close();
34    }

35}

从以上示例代码中,我们可以看到对于nio非常重要的两个核心概念:通道与缓冲区。

1)通道
     Channel是对原I/O包中的流的模拟,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。
   通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类) 而通道可以用于读、写或者同时用于读写。
     因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在UNIX模型中,底层操作系统通道是双向的。

2)缓冲区
       NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问NIO中的数据,您都是将它放到缓冲区中。 
   缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 
   最常用的缓冲区类型是ByteBuffer 一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置) 
   ByteBuffer不是NIO中唯一的缓冲区类型。事实上,对于每一种基本Java类型都有一种缓冲区类型:
   ByteBuffer 
   CharBuffer 
   ShortBuffer 

   IntBuffer 
   LongBuffer 
   FloatBuffer 
   DoubleBuffer 
   每一个Buffer类都是Buffer接口的一个实例。 

文件的读写

nio读取文件涉及三个步骤:
   (1) FileInputStream获取Channel
   (2) 创建Buffer
   (3) 将数据从Channel读到Buffer 中。

文件的写操作与读操作类似。

下面我以文件的拷贝为例,展示一下nio的读写过程:

 1import java.io.FileInputStream;
 2import java.io.FileNotFoundException;
 3import java.io.FileOutputStream;
 4import java.io.IOException;
 5import java.nio.ByteBuffer;
 6import java.nio.channels.FileChannel;
 7
 8/**
 9 * 将一个文件的所有内容拷贝到另一个文件中。
10 * 
11 * 基本步骤:
12 * 1.得到输入输出通道,创建缓冲区
13 * 2.从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件.此过程需不断循环直到源文件结束
14 * 
15 * @author greatjone
16 */

17public class CopyFile {
18    public static void copy(String file,String copyfile) throws IOException{
19         // 获取源文件和目标文件的输入输出流
20        FileInputStream fin = new FileInputStream(file);
21        FileOutputStream fout = new FileOutputStream(copyfile);
22
23        // 获取输入输出通道
24        FileChannel fcin = fin.getChannel();
25        FileChannel fcout = fout.getChannel();
26
27        // 创建缓冲区
28        ByteBuffer buffer = ByteBuffer.allocate(1024);
29
30        while (true{
31            // clear方法重设缓冲区,使它可以接受读入的数据
32            buffer.clear();
33
34            // 从输入通道中将数据读到缓冲区
35            int r = fcin.read(buffer);
36
37            // read方法返回读取的字节数,可能为零,如果该通道已到达流的末尾,则返回-1
38            if (r == -1{
39                break;
40            }

41            
42            // flip方法让缓冲区可以将新读入的数据写入另一个通道
43            buffer.flip();
44
45            // 从输出通道中将数据写入缓冲区
46            fcout.write(buffer);
47        }

48    }

49}

50

--
we drink green tea