首页 > 代码库 > NIO入门系列之第4章:缓冲区内部细节
NIO入门系列之第4章:缓冲区内部细节
4.1 概述
本节将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。
状态变量是前一节中提到的"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。
在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这时使用访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()。
在本节中,您将学习关于NIO 中的状态变量和访问方法的内容。我们将描述每一个组件,并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂,但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记—即使用字节数组和索引变量,现在它已在 NIO 中内部地处理了。
4.2 状态变量
可以用三个值指定缓冲区在任意时刻的状态:
position
limit
capacity
参数 | 写模式 | 读模式 |
位置(position) | 当前缓冲区的位置,将从position的下一个位置写数据 | 当前缓冲区读取的位置,将从此位置后,读取数据 |
容量(capacity) | 缓冲区的总量上限 | 缓冲区的总量上限 |
上限(capacity) | 缓冲区的实际上限,它总是小于或等于容量。通常情况下,和容量相等 | 代表可读取的总容量,和上次写入的数据量相等 |
这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量,还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中,我们假定要将数据从一个输入通道拷贝到一个输出通道。
4.3 Position
您可以回想一下,缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的position 将被设置为5,指向数组的第六个元素。
4.4 Limit
limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position 总是小于或者等于 limit。
4.5 Capacity
缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小—或者至少是指定了准许我们使用的底层数组的容量。
limit 决不能大于 capacity。
4.6 观察变量
我们首先观察一个新创建的缓冲区。出于本例子的需要,我们假设这个缓冲区的总容量为8个字节。 Buffer 的状态如下所示:
回想一下,limit 决不能大于 capacity,此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。
position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示:
由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。
4.7 第一次读取
现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position就增加到 3,如下所示:
limit 没有改变。
4.8 第二次读取
在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2:
limit 没有改变。
4.9 flip
现在我们要将数据写到输出通道中。在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事:
1.它将 limit 设置为当前 position。
2.它将 position 设置为 0。
前一小节中的图显示了在flip 之前缓冲区的情况。下面是在flip 之后的缓冲区:
我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。
4.10 第一次写入
在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4,而 limit 不变,如下所示:
4.11 第二次写入
我们只剩下一个字节可写了。limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得position 增加到 5,并保持 limit 不变,如下所示:
4.12 clear
最后一步是调用缓冲区的clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情:
1.它将 limit 设置为与 capacity 相同。
2.它设置 position 为 0。
下图显示了在调用 clear() 后缓冲区的状态:
缓冲区现在可以接收新的数据了。
对比rewind()、clear()和flip()函数
rewind()将position置零,并清除标志位(mark)。它的作用在于为提取Buffer的有效数据做准备。
clear()也将position置零,同时将limit设置为capacity的大小,并清除标志位(mark)。由于清空了limit,因此便无法的值Buffer内哪些数据是真实有效的。这个方法用于为重新写Buffer做准备。
flip()先将limit设置到position所在的位置,然后将position置为零,并清出标志位mark。它通常在读写转换时使用。
rewind() | clear() | flip() | |
position | 置零 | 置零 | 置零 |
mark | 清空 | 清空 | 清空 |
limit | 未改动 | 设置为capacity | 设置为position |
作用 | 为提取Buffer的有效数据做准备 | 为重新写Buffer做准备 | 在读写转换时使用 |
4.13 访问方法
到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。
或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。
在本节的最后,我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。
4.14 get() 方法
ByteBuffer 类中有四个 get() 方法:
byte get();
ByteBuffer get( byte dst[] );
ByteBuffer get( byte dst[], intoffset, int length );
byte get( int index );
第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。
此外,我们认为前三个get() 方法是相对的,而最后一个方法是绝对的。相对意味着 get() 操作服从 limit 和 position 值—更明确地说,字节是从当前 position 读取的,而 position 在 get 之后会增加。另一方面,一个绝对方法会忽略 limit 和 position 值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。
上面列出的方法对应于ByteBuffer 类。其他类有等价的get() 方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的类型。
4.15 put()方法
ByteBuffer 类中有五个 put() 方法:
ByteBuffer put( byte b );
ByteBuffer put( byte src[] );
ByteBuffer put( byte src[], intoffset, int length );
ByteBuffer put( ByteBuffer src);
ByteBuffer put( int index, byte b );
第一个方法写入(put)单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源 ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的位置。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。
与 get() 方法一样,我们将把 put() 方法划分为相对或者绝对的。前四个方法是相对的,而第五个方法是绝对的。
上面显示的方法对应于ByteBuffer 类。其他类有等价的put() 方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。
4.16 类型化的 get() 和 put() 方法
除了前些小节中描述的get() 和 put() 方法, ByteBuffer 还有用于读写不同类型的值的其他方法,如下所示:
getByte()
getChar()
getShort()
getInt()
getLong()
getFloat()
getDouble()
putByte()
putChar()
putShort()
putInt()
putLong()
putFloat()
putDouble()
事实上,这其中的每个方法都有两种类型:一种是相对的,另一种是绝对的。它们对于读取格式化的二进制数据(如图像文件的头部)很有用。
您可以在例子程序TypesInByteBuffer.java 中看到这些方法的实际应用。
// TypesInByteBuffer import java.io.*; import java.nio.*; import java.nio.channels.*; public class TypesInByteBuffer { static public void main( String args[] ) throws Exception { ByteBuffer buffer = ByteBuffer.allocate( 64 ); buffer.putInt( 30 ); buffer.putLong( 7000000000000L ); buffer.putDouble( Math.PI ); buffer.putInt( 40 ); buffer.flip(); //按照顺序获取—相对位置 System.out.println( buffer.getInt() ); System.out.println( buffer.getLong() ); System.out.println( buffer.getDouble() ); System.out.println( buffer.getInt() ); //按照顺序获取—绝对位置,与上面的结果一样 System.out.println( buffer.getInt(0));//int类型为4byte System.out.println( buffer.getLong(4) );// long类型为4byte System.out.println( buffer.getDouble(12) );// double类型为4byte System.out.println( buffer.getInt(20) ); } }
结果:
{"hb":[0,0,0,30,0,0,6,93,-48,-125,112,0,64,9,33,-5,84,68,45,24,0,0,0,40,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"offset":0,"isReadOnly":false,"bigEndian":true,"nativeByteOrder":false,"mark":-1,"position":24,"limit":64,"capacity":64,"address":0}
30
7000000000000
3.141592653589793
40
30
7000000000000
3.141592653589793
40
4.17 缓冲区的使用:一个内部循环
下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
while (true) { buffer.clear(); int r = fcin.read( buffer ); if (r==-1) { break; } buffer.flip(); fcout.write( buffer ); }
read() 和 write() 调用得到了极大的简化,因为许多工作细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。