JAVA NEW IO (nio)
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:
容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能
被改变。
上界(Limit)
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新。
标记(Mark)
一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position =
mark。标记在设定前是未定义的(undefined)。
这四个属性之间总是遵循以下关系:
0 <= mark <= position <= limit <= capacity
package java.nio;
public abstract class Buffer {
public final int capacity( )
public final int position( )
public final Buffer position (int newPosition)
public final int limit( )
public final Buffer limit (int newLimit)
public final Buffer mark( )
public final Buffer reset( )
public final Buffer clear( )
public final Buffer flip( )
public final Buffer rewind( )
public final int remaining( )
public final boolean hasRemaining( )
public abstract boolean isReadOnly( );
}
关于这个 API 有一点要注意的是,像 clear()这类函数,您通常应当返回 void,而不
是 Buffer 引用。这些函数将引用返回到它们在(this)上被引用的对象。这是一个允许级
联调用的类设计方法。级联调用允许这种类型的代码:
buffer.mark( );
buffer.position(5);
buffer.reset( );
被简写为:
buffer.mark().position(5).reset( );
上界属性指明了缓冲区有效内容的末端。我们需要将上界属性设置为当前位置,然后将位
置重置为 0。我们可以人工用下面的代码实现:
buffer.limit(buffer.position()).position(0);
但这种从填充到释放状态的缓冲区翻转是 API 设计者预先设计好的,他们为我们提供了
一个非常便利的函数:
Buffer.flip();
Flip()函数将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素
的释放状态。
Rewind()函数与 flip()相似,但不影响上界属性。它只是将位置值设回 0。您可以使
用 rewind()后退,重读已经被翻转的缓冲区中的数据。
如果将缓冲区翻转两次会怎样呢?它实际上会大小变为 0。按照图 2.5 的相同步骤对缓冲
区进行操作;把上界设为位置的值,并把位置设为 0。上界和位置都变成 0。尝试对缓冲区上
位置和上界都为 0 的 get()操作会导致 BufferUnderflowException 异常。而 put()则
会导致 BufferOverflowException 异常。
布尔函数 hasRemaining()会在释放缓冲区时告诉您是否已经达到缓冲区的上界。以下
是一种将数据元素从缓冲区释放到一个数组的方法(在 2.1.10 小节中,我们将学到进行批量
传输的更高效的方法)。
for (int i = 0; buffer.hasRemaining( ), i++) {
myByteArray [i] = buffer.get( );
}
作为选择,remaining()函数将告知您从当前位置到上界还剩余的元素数目。您也可以
通过下面的循环来释放图 2-5 所示的缓冲区。
int count = buffer.remaining( );
for (int i = 0; i < count, i++) {
myByteArray [i] = buffer.get( );
}
一旦缓冲区对象完成填充并释放,它就可以被重新使用了。Clear()函数将缓冲区重置
为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设
回 0。这使得缓冲区可以被重新填入。
demo:
package nio;
import java.nio.CharBuffer;
public class NIO_1 {
private static int index = 0;
private static String[] strings = { "A random string value","The product of an infinite number of monkeys","Hey hey we're the Monkees","Opening act for the Monkees: Jimi Hendrix",
"'Scuse me while I kiss this fly","Help Me! Help Me!", };
private static void drainBuffer(CharBuffer buffer) {
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
System.out.println("");
}
private static boolean fillBuffer(CharBuffer buffer) {
if (index >= strings.length) {
return (false);
}
String string = strings[index++];
for (int i = 0; i < string.length(); i++) {
buffer.put(string.charAt(i));
}
return (true);
}
public static void main(String[] argv) throws Exception {
CharBuffer buffer = CharBuffer.allocate(100);
while (fillBuffer(buffer)) {
buffer.flip();
drainBuffer(buffer);
buffer.clear();
}
}
}
有时候比较两个缓冲区所包含的数据是很有必要的。所有的缓冲区都提供了一个常规的
equals( )函数用以测试两个缓冲区的是否相等,以及一个 compareTo( )函数用以比较缓冲区。
public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public boolean equals (Object ob)
public int compareTo (Object ob)
}
两个缓冲区可用下面的代码来测试是否相等:
if (buffer1.equals (buffer2)) {
doSomething( );
}
如果每个缓冲区中剩余的内容相同,那么 equals( )函数将返回 true,否则返回 false。
因为这个测试是用于严格的相等而且是可换向的。前面的程序清单中的缓冲区名称可以颠倒,
并会产生相同的结果。
两个缓冲区被认为相等的充要条件是:
两个对象类型相同。包含不同数据类型的 buffer 永远不会相等,而且 buffer
绝不会等于非 buffer 对象。
两个对象都剩余同样数量的元素。Buffer 的容量不需要相同,而且缓冲区中剩
余数据的索引也不必相同。但每个缓冲区中剩余元素的数目(从位置到上界)必须相
同。
在每个缓冲区中应被 Get()函数返回的剩余数据元素序列必须一致。
如果不满足以上任意条件,就会返回 false。
缓冲区也支持用 compareTo( )函数以词典顺序进行比较。这一函数在缓冲区参数小
于,等于,或者大于引用 compareTo( )的对象实例时,分别返回一个负整数,0 和正整
数。这些就是所有典型的缓冲区所实现的 java.lang.Comparable 接口语义。这意味着缓
冲区数组可以通过调用 java.util.Arrays.sort()函数按照它们的内容进行排序。
与 equals( )相似,compareTo( )不允许不同对象间进行比较。但 compareTo( )更为严格:如
果您传递一个类型错误的对象,它会抛出 ClassCastException 异常,但 equals( )只会返回
false。
比较是针对每个缓冲区内剩余数据进行的,与它们在 equals( )中的方式相同,直到不相等
的元素被发现或者到达缓冲区的上界。如果一个缓冲区在不相等元素发现前已经被耗尽,较短
的缓冲区被认为是小于较长的缓冲区。不像 equals( ),compareTo( )不可交换:顺序问题。在本
例中,一个小于零的结果表明 buffer2 小于 buffer1,而表达式的值就会是 true:
if (buffer1.compareTo (buffer2) < 0) {
doSomething( );
}
如果前面的代码被应用到图 2-10 所示的缓冲区中,结果会是 0,而 if 语句将毫无用
处。被应用到图 2-11 的缓冲区的相同测试将会返回一个正数(表明 buffer2 大于
buffer1),而这个表达式也会被判断为 false。
2.1.10 批量移动
缓冲区的涉及目的就是为了能够高效传输数据。一次移动一个数据元素,如例 2-1 所示
的那样并不高效。如您在下面的程序清单中所看到的那样,buffer API 提供了向缓冲区内
外批量移动数据元素的函数。
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
// This is a partial API listing
public CharBuffer get (char [] dst)
public CharBuffer get (char [] dst, int offset, int length)
public final CharBuffer put (char[] src)
public CharBuffer put (char [] src, int offset, int length)
public CharBuffer put (CharBuffer src)
public final CharBuffer put (String src)
public CharBuffer put (String src, int start, int end)
}
有两种形式的 get( )可供从缓冲区到数组进行的数据复制使用。第一种形式只将一个数组
作为参数,将一个缓冲区释放到给定的数组。第二种形式使用 offset 和 length 参数来指
定目标数组的子区间。这些批量移动的合成效果与前文所讨论的循环是相同的,但是这些方法
可能高效得多,因为这种缓冲区实现能够利用本地代码或其他的优化来移动数据。
批量移动总是具有指定的长度。也就是说,您总是要求移动固定数量的数据元素。当参看
程序签名时这一点还不明显,但是对 get( )的这一引用:
buffer.get(myArray);
等价于:
buffer.get(myArray,0,myArray.length);
如果您所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不
变,同时抛出 BufferUnderflowException 异常。因此当您传入一个数组并且没有指定长
度,您就相当于要求整个数组被填充。如果缓冲区中的数据不够完全填满数组,您会得到一个
异常。这意味着如果您想将一个小型缓冲区传入一个大型数组,您需要明确地指定缓冲区中剩
余的数据长度。上面的第一个例子不会如您第一眼所推出的结论那样,将缓冲区内剩余的数据
元素复制到数组的底部。要将一个缓冲区释放到一个大数组中,要这样做:
char [] bigArray = new char [1000];
// Get count of chars remaining in the buffer
int length = buffer.remaining( );
// Buffer is known to contain < 1,000 chars
buffer.get (bigArrray, 0, length);
// Do something useful with the data
processData (bigArray, length);
记住在调用 get( )之前必须查询缓冲区中的元素数量(因为我们需要告知 processData( )被
放置在 bigArray 中的字符个数)。调用 get( )会向前移动缓冲区的位置属性,所以之后调用
remaining( )会返回 0。get( )的批量版本返回缓冲区的引用,而不是被传送的数据元素的计数,
以减轻级联调用的困难。
另一方面,如果缓冲区存有比数组能容纳的数量更多的数据,您可以重复利用如下文所示
的程序块进行读取:
char [] smallArray = new char [10];
while (buffer.hasRemaining( )) {
int length = Math.min (buffer.remaining( ), smallArray.length);
buffer.get (smallArray, 0, length);
processData (smallArray, length);
}
复制缓冲区
public abstract class CharBuffer
extends Buffer implements CharSequence, Comparable
{
// This is a partial API listing
public abstract CharBuffer duplicate( );
public abstract CharBuffer asReadOnlyBuffer( );
public abstract CharBuffer slice( );
}
Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元
素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数
据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据
视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。
note:
复制一个缓冲区会创建一个新的 Buffer 对象,但并不
复制数据。原始缓冲区和副本都会操作同样的数据元
素。
例如:
CharBuffer buffer = CharBuffer.allocate (8);
buffer.position (3).limit (6).mark( ).position (5);
CharBuffer dupeBuffer = buffer.duplicate( );
buffer.clear( );
您 可 以 使 用 asReadOnlyBuffer() 函 数 来 生 成 一 个 只 读 的 缓 冲 区 视 图 。 这 与
duplicate()相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly()函数
将 会 返 回 true 。 对 这 一 只 读 缓 冲 区 的 put() 函 数 的 调 用 尝 试 会 导 致 抛 出
ReadOnlyBufferException 异常。
分割缓冲区与复制相似,但 slice()创建一个从原始缓冲区的当前位置开始的新缓冲
区,并且其容量是原始缓冲区的剩余元素数量(limit-position)。这个新缓冲区与原始
缓冲区共享一段数据元素子序列。分割出来的缓冲区也会继承只读和直接属性。图 2-13 显示
了以与下面代码相似的代码所生成的分割缓冲区:
CharBuffer buffer = CharBuffer.allocate (8);
buffer.position (3).limit (5);
CharBuffer sliceBuffer = buffer.slice( );
要创建一个映射到数组位置 12-20(9 个元素)的 buffer 对象,应使用下面的代码实
现:
char [] myBuffer = new char [100];
CharBuffer cb = CharBuffer.wrap (myBuffer);
cb.position(12).limit(21);
CharBuffer sliced = cb.slice( );
以下是 ByteBuffer 的完整 API:
package java.nio;
public abstract class ByteBuffer extends Buffer
implements Comparable
{
public static ByteBuffer allocate (int capacity)
public static ByteBuffer allocateDirect (int capacity)
public abstract boolean isDirect( );
public static ByteBuffer wrap (byte[] array, int offset, int length)
public static ByteBuffer wrap (byte[] array)
public abstract ByteBuffer duplicate( );
public abstract ByteBuffer asReadOnlyBuffer( );
public abstract ByteBuffer slice( );
public final boolean hasArray( )
public final byte [] array( )
public final int arrayOffset( )
public abstract byte get( );
public abstract byte get (int index);
public ByteBuffer get (byte[] dst, int offset, int length)
public ByteBuffer get (byte[] dst, int offset, int length)
public abstract ByteBuffer put (byte b);
public abstract ByteBuffer put (int index, byte b);
public ByteBuffer put (ByteBuffer src)
public ByteBuffer put (byte[] src, int offset, int length)
public final ByteBuffer put (byte[] src)
public final ByteOrder order( )
public final ByteBuffer order (ByteOrder bo)
public abstract CharBuffer asCharBuffer( );
public abstract ShortBuffer asShortBuffer( );
public abstract IntBuffer asIntBuffer( );
public abstract LongBuffer asLongBuffer( );
public abstract FloatBuffer asFloatBuffer( );
public abstract DoubleBuffer asDoubleBuffer( );
public abstract char getChar( );
public abstract char getChar (int index);
public abstract ByteBuffer putChar (char value);
public abstract ByteBuffer putChar (int index, char value);
public abstract short getShort( );
public abstract short getShort (int index);
public abstract ByteBuffer putShort (short value);
public abstract ByteBuffer putShort (int index, short value);
public abstract int getInt( );
public abstract int getInt (int index);
public abstract ByteBuffer putInt (int value);
public abstract ByteBuffer putInt (int index, int value);
public abstract long getLong( );
public abstract long getLong (int index);
public abstract ByteBuffer putLong (long value);
public abstract ByteBuffer putLong (int index, long value);
public abstract float getFloat( );
public abstract float getFloat (int index);
public abstract ByteBuffer putFloat (float value);
public abstract ByteBuffer putFloat (int index, float value);
public abstract double getDouble( );
public abstract double getDouble (int index);
public abstract ByteBuffer putDouble (double value);
public abstract ByteBuffer putDouble (int index, double value);
public abstract ByteBuffer compact( );
public boolean equals (Object ob) {
public int compareTo (Object ob) {
public String toString( )
public int hashCode( )
}
非字节类型的基本类型,除了布尔型
3都是由组合在一起的几个字节组成的。这些数据类
型及其大小总结在表 2-1 中。
表 2-1.基本数据类型及其大小
数据类型 大小(以字节表示)
Byte 1
Char 2
Short 2
Int 4
Long 8
Float 4
Double 8
在 java.nio 中,字节顺序由 ByteOrder 类封装。
package java.nio;
public final class ByteOrder
{
public static final ByteOrder BIG_ENDIAN
public static final ByteOrder LITTLE_ENDIAN
public static ByteOrder nativeOrder( )
public String toString( )
}
ByteOrder 类定义了决定从缓冲区中存储或检索多字节数值时使用哪一字节顺序的常
量。这个类的作用就像一个类型安全的枚举。它定义了以其本身实例预初始化的两个 public
区域。只有这两个 ByteOrder 实例总是存在于 JVM 中,因此它们可以通过使用--操作符进
行比较。如果您需要知道 JVM 运行的硬件平台的固有字节顺序,请调用静态类函数
nativeOrder()。它将返回两个已确定常量中的一个。调用 toString()将返回一个包含两
个文字字符串 BIG_ENDIAN 或者 LITTLE_ENDIAN 之一的 String。
System.out.println(ByteOrder.nativeOrder());
将会打印出 LITTLE_ENDIAN 和 BIG_ENDIAN中的一种
视图缓冲区
就像我们已经讨论的那样,I/O 基本上可以归结成组字节数据的四处传递。在进行大数据
量的 I/O 操作时,很又可能您会使用各种 ByteBuffer 类去读取文件内容,接收来自网络连
接的数据,等等。一旦数据到达了您的 ByteBuffer,您就需要查看它以决定怎么做或者在
将它发送出去之前对它进行一些操作。ByteBuffer 类提供了丰富的 API 来创建视图缓冲
区。
视图缓冲区通过已存在的缓冲区对象实例的工厂方法来创建。这种视图对象维护它自己的
属性,容量,位置,上界和标记,但是和原来的缓冲区共享数据元素。我们已经在 2.3 节见
过了这样的简单例子,在例子中一个缓冲区被复制和切分。但是 ByteBuffer 类允许创建视
图来将 byte 型缓冲区字节数据映射为其它的原始数据类型。例如,asLongBuffer()函数
创建一个将八个字节型数据当成一个 long 型数据来存取的视图缓冲区。
下面列出的每一个工厂方法都在原有的 ByteBuffer 对象上创建一个视图缓冲区。调用
其中的任何一个方法都会创建对应的缓冲区类型,这个缓冲区是基础缓冲区的一个切分,由基
础缓冲区的位置和上界决定。新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型
中组成一个数据类型的字节数(参见表 2-1)。在切分中任一个超过上界的元素对于这个视图
缓冲区都是不可见的。视图缓冲区的第一个元素从创建它的 ByteBuffer 对象的位置开始
(positon()函数的返回值)。具有能被自然数整除的数据元素个数的视图缓冲区是一种较
好的实现。
public abstract class ByteBuffer
extends Buffer implements Comparable
{
// This is a partial API listing
public abstract CharBuffer asCharBuffer( );
public abstract ShortBuffer asShortBuffer( );
public abstract IntBuffer asIntBuffer( );
public abstract LongBuffer asLongBuffer( );
public abstract FloatBuffer asFloatBuffer( );
public abstract DoubleBuffer asDoubleBuffer( );
}
下面的代码创建了一个 ByteBuffer 缓冲区的 CharBuffer 视图,如图 Figure 2-16
所示(Example 2-2 将这个框架用到了更大的范围)
ByteBuffer byteBuffer = ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );
创建一个 ByteBuffer 的字符视图
package com.ronsoft.books.nio.buffers;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.ByteOrder;
/**
* Test asCharBuffer view.
*
* Created May 2002
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class BufferCharView
{
public static void main (String [] argv)
throws Exception
{
ByteBuffer byteBuffer =
ByteBuffer.allocate (7).order (ByteOrder.BIG_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer( );
// Load the ByteBuffer with some bytes
byteBuffer.put (0, (byte)0);
byteBuffer.put (1, (byte)'H');
byteBuffer.put (2, (byte)0);
byteBuffer.put (3, (byte)'i');
byteBuffer.put (4, (byte)0);
byteBuffer.put (5, (byte)'!');
byteBuffer.put (6, (byte)0);
println (byteBuffer);
println (charBuffer);
}
// Print info about a buffer
private static void println (Buffer buffer)
{
System.out.println ("pos=" + buffer.position( )
+ ", limit=" + buffer.limit( )
+ ", capacity=" + buffer.capacity( )
+ ": '" + buffer.toString( ) + "'");
}
}
pos=0, limit=7, capacity=7: 'java.nio.HeapByteBuffer[pos=0 lim=7 cap=7]'
pos=0, limit=3, capacity=3: 'Hi!'
运行 BufferCharView 程序的输出是:
pos=0, limit=7, capacity=7: 'java.nio.HeapByteBuffer[pos=0 lim=7 cap=7]'
pos=0, limit=3, capacity=3: 'Hi!
一 旦 您 得 到 了 视 图 缓 冲 区 , 您 可 以 用
duplicate() , slice() 和
asReadOnlyBuffer()函数创建进一步的子视图,就像 2.3 节所讨论的那样。
无论何时一个视图缓冲区存取一个 ByteBuffer 的基础字节,这些字节都会根据这个视
图缓冲区的字节顺序设定被包装成一个数据元素。当一个视图缓冲区被创建时,视图创建的同
时它也继承了基础 ByteBuffer 对象的字节顺序设定。这个视图的字节排序不能再被修改。
在图 2-16 中,您可以看到基础 ByteBuffer 对象中的两个字节映射成 CharBuffer 对象中
的一个字符。字节顺序设定决定了这些字节对是怎么样被组合成字符型变量的。
通道 channel
通道是访问 I/O 服务的导管。正如我们在第一章中所讨论的,I/O 可以分为广义的两大类别:
File I/O 和 Stream I/O。那么相应地有两种类型的通道也就不足为怪了,它们是文件(file)通道和
套接字(socket)通道。如果您参考一下图 3-2,您就会发现有一个 FileChannel 类和三个 socket 通
道类:SocketChannel、ServerSocketChannel 和 DatagramChannel。
通道可以以多种方式创建。Socket 通道有可以直接创建新 socket 通道的工厂方法。但是一个
FileChannel 对象却只能通过在一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream
对象上调用 getChannel( )方法来获取。您不能直接创建一个 FileChannel 对象。File 和 socket 通道会
在后面的章节中予以详细讨论。
SocketChannel sc = SocketChannel.open( );
sc.connect (new InetSocketAddress ("somehost", someport));
ServerSocketChannel ssc = ServerSocketChannel.open( );
ssc.socket( ).bind (new InetSocketAddress (somelocalport));
DatagramChannel dc = DatagramChannel.open( );
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel( );
java.net 的 socket 类也有新的 getChannel( )方法。这些方法虽然
能返回一个相应的 socket 通道对象,但它们却并非新通道的来源,
RandomAccessFile.getChannel( )方法才是。只有在已经有通道存在的时候,它们才返回与
一个 socket 关联的通道;它们永远不会创建新通道。
我们在第二章的学习中已经知道了,通道将数据传输给 ByteBuffer 对象或者从 ByteBuffer 对象
获取数据进行传输。
将图 3-2 中大部分零乱内容移除可以得到图 3-3 所示的 UML 类图。子接口 API 代码如下:
public interface ReadableByteChannel
extends Channel
{
public int read (ByteBuffer dst) throws IOException;
}
public interface WritableByteChannel
extends Channel
{
public int write (ByteBuffer src) throws IOException;
}
public interface ByteChannel
extends ReadableByteChannel, WritableByteChannel
{
}
通道可以是单向(unidirectional)或者双向的(bidirectional)。一个 channel 类可能实现定义
read( )方法的 ReadableByteChannel 接口,而另一个 channel 类也许实现 WritableByteChannel 接口以
提供 write( )方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果
一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。
buffer.compact()经常跟 buffer.flip()一起使用,用来将数据从一个地方转移到另一个地方:
buffer.compact():
The bytes between the buffer's current position and its limit, if any, are copied to the beginning of the buffer. That is, the byte at index p = position() is copied to index zero, the byte at index p + 1 is copied to index one, and so forth until the byte at index limit() - 1 is copied to index n = limit() - 1 - p. The buffer's position is then set to n+1 and its limit is set to its capacity. The mark, if defined, is discarded.
The buffer's position is set to the number of bytes copied, rather than to zero, so that an invocation of this method can be followed immediately by an invocation of another relative put method.
Invoke this method after writing data from a buffer in case the write was incomplete. The following loop, for example, copies bytes from one channel to another via the buffer buf:
buf.clear(); // Prepare buffer for use
while (in.read(buf) >= 0 || buf.position != 0) {
buf.flip();
out.write(buf);
buf.compact(); // In case of partial write
}
buffer.flip():
Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.
After a sequence of channel-read or put operations, invoke this method to prepare for a sequence of channel-write or relative get operations. For example:
buf.put(magic); // Prepend header
in.read(buf); // Read data into rest of buffer
buf.flip(); // Flip buffer
out.write(buf); // Write header + data to channel
This method is often used in conjunction with the compact method when transferring data from one place to another.
ByteChannel 的 read( ) 和 write( )方法使用 ByteBuffer 对象作为参数。两种方法均返回已传输的
字节数,可能比缓冲区的字节数少甚至可能为零。缓冲区的位置也会发生与已传输字节相同数量的
前移。如果只进行了部分传输,缓冲区可以被重新提交给通道并从上次中断的地方继续传输。该过
程重复进行直到缓冲区的 hasRemaining( )方法返回 false 值。例 3-1 表示了如何从一个通道复制
数据到另一个通道。
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.Channels;
import java.io.IOException;
/**
* Test copying between channels.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class ChannelCopy
{
/**
* This code copies data from stdin to stdout. Like the 'cat'
* command, but without any useful options.
*/
public static void main (String [] argv)
throws IOException
{
ReadableByteChannel source = Channels.newChannel (System.in);
WritableByteChannel dest = Channels.newChannel (System.out);
channelCopy1 (source, dest);
// alternatively, call channelCopy2 (source, dest);
source.close( );
dest.close( );
}
/**
* Channel copy method 1. This method copies data from the src
* channel and writes it to the dest channel until EOF on src.
* This implementation makes use of compact( ) on the temp buffer
* to pack down the data if the buffer wasn't fully drained. This
* may result in data copying, but minimizes system calls. It also
* requires a cleanup loop to make sure all the data gets sent.
*/
private static void channelCopy1 (ReadableByteChannel src,
WritableByteChannel dest)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocateDirect (16 * 1024);
while (src.read (buffer) != -1) {
// Prepare the buffer to be drained
buffer.flip( );
// Write to the channel; may block
dest.write (buffer);
// If partial transfer, shift remainder down
// If buffer is empty, same as doing clear( )
buffer.compact( );
}
// EOF will leave buffer in fill state
buffer.flip( );
// Make sure that the buffer is fully drained
while (buffer.hasRemaining( )) {
dest.write (buffer);
}
}
/**
* Channel copy method 2. This method performs the same copy, but
* assures the temp buffer is empty before reading more data. This
* never requires data copying but may result in more systems calls.
* No post-loop cleanup is needed because the buffer will be empty
* when the loop is exited.
*/
private static void channelCopy2 (ReadableByteChannel src,
WritableByteChannel dest)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocateDirect (16 * 1024);
while (src.read (buffer) != -1) {
// Prepare the buffer to be drained
buffer.flip( );
// Make sure that the buffer was fully drained
while (buffer.hasRemaining( )) {
dest.write (buffer);
}
// Make the buffer empty, ready for filling
buffer.clear( );
}
}
}
通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会
让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向
流的(stream-oriented)的通道,如 sockets 和 pipes 才能使用非阻塞模式。
Scatter/Gather(名字是相对于缓冲区的)
Scatter:分发,发散到多个缓冲区,也就是读取的操作,把读取到的内容分别存储到多个缓冲区
Gather:汇聚:也就是将多个缓冲区的数据读取到一起,然后写出去,是写操作
通道提供了一种被称为 Scatter/Gather 的重要新功能(有时也被称为矢量 I/O)。Scatter/Gather
是一个简单却强大的概念(参见 1.4.1.1 节),它是指在多个缓冲区上实现一个简单的 I/O 操作。对
于一个 write 操作而言,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。缓冲
区本身并不需要具备这种 gather 的能力(通常它们也没有此能力)。该 gather 过程的效果就好比全
部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。对于 read 操作而言,从
通道读取的数据会按顺序被散布(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数
据或者缓冲区的最大空间被消耗完。
大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。当您在一个通道上请求一个
Scatter/Gather 操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大
的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather 应该使用直接的 ByteBuffers 以从
本地 I/O 获取最大性能优势。
将 scatter/gather 接口添加到图 3-3 的 UML 类图中可以得到图 3-4。下面的代码描述了 scatter 是
如何扩展读操作的,以及 gather 是如何基于写操作构建的:
public interface ScatteringByteChannel
extends ReadableByteChannel
{
public long read (ByteBuffer [] dsts)
throws IOException;
public long read (ByteBuffer [] dsts, int offset, int length)
throws IOException;
}
public interface GatheringByteChannel
extends WritableByteChannel
{
public long write(ByteBuffer[] srcs)
throws IOException;
public long write(ByteBuffer[] srcs, int offset, int length)
throws IOException;
}
带 offset 和 length 参数版本的 read( ) 和 write( )方法使得我们可以使用缓冲区阵列的子集
缓冲区。这里的 offset 值指哪个缓冲区将开始被使用,而不是指数据的 offset。这里的 length 参
数指示要使用的缓冲区数量。举个例子,假设我们有一个五元素的 fiveBuffers 阵列,它已经被
初始化并引用了五个缓冲区,下面的代码将会写第二个、第三个和第四个缓冲区的内容:
int bytesRead = channel.write (fiveBuffers, 1, 3);
使用得当的话,Scatter/Gather 会是一个极其强大的工具。它允许您委托操作系统来完成辛苦
活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这
是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了您来回移动数据的
工作,也就避免了缓冲区拷贝和减少了您需要编写、调试的代码数量。既然您基本上通过提供数据
容器引用来组合数据,那么按照不同的组合构建多个缓冲区阵列引用,各种数据区块就可以以不同
的方式来组合了。例 3-2 很好地诠释了这一点:
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.channels.GatheringByteChannel;
import java.io.FileOutputStream;
import java.util.Random;
import java.util.List;
import java.util.LinkedList;
/**
* Demonstrate gathering write using many buffers.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class Marketing
{
private static final String DEMOGRAPHIC = "blahblah.txt";
// "Leverage frictionless methodologies"
public static void main (String [] argv)
throws Exception
{
int reps = 10;
if (argv.length > 0) {
reps = Integer.parseInt (argv [0]);
}
FileOutputStream fos = new FileOutputStream (DEMOGRAPHIC);
GatheringByteChannel gatherChannel = fos.getChannel( );
// Generate some brilliant marcom, er, repurposed content
ByteBuffer [] bs = utterBS (reps);
// Deliver the message to the waiting market
while (gatherChannel.write (bs) > 0) {
// Empty body
// Loop until write( ) returns zero
}
System.out.println ("Mindshare paradigms synergized to "
+ DEMOGRAPHIC);
fos.close( );
}
// ------------------------------------------------
// These are just representative; add your own
private static String [] col1 = {
"Aggregate", "Enable", "Leverage",
"Facilitate", "Synergize", "Repurpose",
"Strategize", "Reinvent", "Harness"
};
private static String [] col2 = {
"cross-platform", "best-of-breed", "frictionless",
"ubiquitous", "extensible", "compelling",
"mission-critical", "collaborative", "integrated"
};
private static String [] col3 = {
"methodologies", "infomediaries", "platforms",
"schemas", "mindshare", "paradigms",
"functionalities", "web services", "infrastructures"
};
private static String newline = System.getProperty ("line.separator");
// The Marcom-atic 9000
private static ByteBuffer [] utterBS (int howMany)
throws Exception
{
List list = new LinkedList( );
for (int i = 0; i < howMany; i++) {
list.add (pickRandom (col1, " "));
list.add (pickRandom (col2, " "));
list.add (pickRandom (col3, newline));
}
ByteBuffer [] bufs = new ByteBuffer [list.size( )];
list.toArray (bufs);
return (bufs);
}
// The communications director
private static Random rand = new Random( );
// Pick one, make a buffer to hold it and the suffix, load it with
// the byte equivalent of the strings (will not work properly for
// non-Latin characters), then flip the loaded buffer so it's ready
// to be drained
private static ByteBuffer pickRandom (String [] strings, String suffix)
throws Exception
{
String string = strings [rand.nextInt (strings.length)];
int total = string.length() + suffix.length( );
ByteBuffer buf = ByteBuffer.allocate (total);
buf.put (string.getBytes ("US-ASCII"));
buf.put (suffix.getBytes ("US-ASCII"));
buf.flip( );
return (buf);
}
}
FileChannel 位置(position)是从底层的文件描述符获得的,该 position 同时被作为通道引用
获取来源的文件对象共享。这也就意味着一个对象对该 position 的更新可以被另一个对象看到:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
public class Marketing {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile ("E:\\程序代码\\0621sql.txt", "r");
// Set the file position
randomAccessFile.seek (1000);
// Create a channel from the file
FileChannel fileChannel = randomAccessFile.getChannel( );
// This will print "1000"
System.out.println ("file pos: " + fileChannel.position( ));
// Change the position using the RandomAccessFile object
randomAccessFile.seek (500);
// This will print "500"
System.out.println ("file pos: " + fileChannel.position( ));
// Change the position using the FileChannel object
fileChannel.position (200);
// This will print "200"
System.out.println ("file pos: " + randomAccessFile.getFilePointer( ));
}
}
输出结果为:
file pos: 1000
file pos: 500
file pos: 200
内存映射文件
新的 FileChannel 类提供了一个名为 map( )的方法,该方法可以在一个打开的文件和一个特殊
类型的 ByteBuffer 之间建立一个虚拟内存映射(第一章中已经归纳了什么是内存映射文件以及它们
如何同虚拟内存交互)。在 FileChannel 上调用 map( )方法会创建一个由磁盘文件支持的虚拟内存
映射(virtual memory mapping)并在那块虚拟内存空间外部封装一个 MappedByteBuffer 对象(参见
图 1-6)。
由 map( )方法返回的 MappedByteBuffer 对象的行为在多数方面类似一个基于内存的缓冲区,只
不过该对象的数据元素存储在磁盘上的一个文件中。调用 get( )方法会从磁盘文件中获取数据,此
数据反映该文件的当前内容,即使在映射建立之后文件已经被一个外部进程做了修改。通过文件映
射看到的数据同您用常规方法读取文件看到的内容是完全一样的。相似地,对映射的缓冲区实现一
个 put( )会更新磁盘上的那个文件(假设对该文件您有写的权限),并且您做的修改对于该文件的
其他阅读者也是可见的。
通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都
package test;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
/**
* Test nonblocking accept( ) using ServerSocketChannel. Start this program,
* then "telnet localhost 1234" to connect to it.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class ChannelAccept {
public static final String GREETING = "Hello I must be going.\r\n";
public static void main(String[] argv) throws Exception {
int port = 8080; // default
if (argv.length > 0) {
port = Integer.parseInt(argv[0]);
}
ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(port));
ssc.configureBlocking(false);
while (true) {
System.out.println("Waiting for connections");
SocketChannel sc = ssc.accept();
if (sc == null) {
// no connections, snooze a while
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from: "
+ sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
}
}
package test;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
/**
* Demonstrate asynchronous connection of a SocketChannel.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class ConnectAsync {
public static void main(String[] argv) throws Exception {
String host = "localhost";
int port = 8080;
if (argv.length == 2) {
host = argv[0];
port = Integer.parseInt(argv[1]);
}
InetSocketAddress addr = new InetSocketAddress(host, port);
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
System.out.println("initiating connection");
sc.connect(addr);
while (!sc.finishConnect()) {
doSomethingUseful();
}
System.out.println("connection established");
// Do something with the connected socket
// The SocketChannel is still nonblocking
sc.close();
}
private static void doSomethingUseful() {
System.out.println("doing something useless");
}
}
让我们从最简单的 ServerSocketChannel 来开始对 socket 通道类的讨论。以下是
ServerSocketChannel 的完整 API:
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
{
public static ServerSocketChannel open( ) throws IOException
public abstract ServerSocket socket( );
public abstract ServerSocket accept( ) throws IOException;
public final int validOps( )
}
SocketChannel
下面开始学习 SocketChannel,它是使用最多的 socket 通道类:
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public static SocketChannel open( ) throws IOException
public static SocketChannel open (InetSocketAddress remote)
throws IOException
public abstract Socket socket( );
public abstract boolean connect (SocketAddress remote)
throws IOException;
public abstract boolean isConnectionPending( );
public abstract boolean finishConnect( ) throws IOException;
public abstract boolean isConnected( );
public final int validOps( )
}
正如 SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的
无连接协议(如 UDP/IP):
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
// This is a partial API listing
public static DatagramChannel open( ) throws IOException
public abstract DatagramSocket socket( );
public abstract DatagramChannel connect (SocketAddress remote)
throws IOException;
public abstract boolean isConnected( );
public abstract DatagramChannel disconnect( ) throws IOException;
public abstract SocketAddress receive (ByteBuffer dst)
throws IOException;
public abstract int send (ByteBuffer src, SocketAddress target)
public abstract int read (ByteBuffer dst) throws IOException;
public abstract long read (ByteBuffer [] dsts) throws IOException;
public abstract long read (ByteBuffer [] dsts, int offset,
int length)
throws IOException;
public abstract int write (ByteBuffer src) throws IOException;
public abstract long write(ByteBuffer[] srcs) throws IOException;
public abstract long write(ByteBuffer[] srcs, int offset,
int length)
throws IOException;
}
创建 DatagramChannel 的模式和创建其他 socket 通道是一样的:调用静态的 open( )方法来创建
一个新实例。新 DatagramChannel 会有一个可以通过调用 socket( )方法获取的对等 DatagramSocket
对象。DatagramChannel 对象既可以充当服务器(监听者)也可以充当客户端(发送者)。如果您
希望新创建的通道负责监听,那么通道必须首先被绑定到一个端口或地址/端口组合上。绑定
DatagramChannel 同绑定一个常规的 DatagramSocket 没什么区别,都是委托对等 socket 对象上的
API 实现的:
DatagramChannel channel = DatagramChannel.open( );
DatagramSocket socket = channel.socket( );
socket.bind (new InetSocketAddress (portNumber));
DatagramChannel 是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己
的目的地址及不依赖其他数据报的数据净荷。与面向流的的 socket 不同,DatagramChannel 可以发
送单独的数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据
包。每个到达的数据报都含有关于它来自何处的信息(源地址)。
一个未绑定的 DatagramChannel 仍能接收数据包。当一个底层 socket 被创建时,一个动态生成
的端口号就会分配给它。绑定行为要求通道关联的端口被设置为一个特定的值(此过程可能涉及安
全检查或其他验证)。不论通道是否绑定,所有发送的包都含有 DatagramChannel 的源地址
(带端口号)。未绑定的 DatagramChannel 可以接收发送给它的端口的包,通常是来回应该通道之
前发出的一个包。已绑定的通道接收发送给它们所绑定的熟知端口(wellknown port)的包。数据
的实际发送或接收是通过 send( )和 receive( )方法来实现的: