在 JDK 1.4 以前, Java 的 IO 操作集中在 java.io 这个包中,是基于流的阻塞( blocking ) API 。对于大多数应用来说,这样的 API 使用很方便,然而,一些对性能要求较高的应用,尤其是服务端应用, 往往需要一个更为有效的方式来处理 IO 。从 JDK 1.4 起, NIO API 作为一个基于缓冲区,并能 提供非阻塞 (non-blocking)IO 操作的 API 被引入。本文对其进行深入的介绍。
NIO API 主要集中在 java.nio 和它的 subpackages 中:
java.nio
定义了 Buffer 及其数据类型相关的子类。其中被 java.nio.channels 中的类用来进行 IO 操作的 ByteBuffer 的作用非常重要。
java.nio.channels
定义了一系列处理 IO 的 Channel 接口以及这些接口在文件系统和网络通讯上的实现。通过 Selector 这个类,还提供了进行非阻塞 IO 操作的办法。这个包可以说是 NIO API 的核心。
java.nio.channels.spi
定义了可用来实现 channel 和 selector API 的抽象类。
java.nio.charset
定义了处理字符编码和解码的类。
java.nio.charset.spi
定义了可用来实现 charset API 的抽象类。
java.nio.channels.spi 和 java.nio.charset.spi 这 两个包主要被用来对现有 NIO API 进行扩展,在实际的使用中,我们一般只和另外的 3 个包打交道。下面将对这 3 个包一一介绍。
Package java.nio
这个包主要定义了 Buffer 及其子类。 Buffer 定义了一个线性存放 primitive type 数据的容器接口。对于除 boolean 以外的其他 primitive type ,都有一个相 应的 Buffer 子 类, ByteBuffer 是其中最重要的一个子类。
下面这张 UML 类图描述了 java.nio 中的类的关系:
Buffer
定义了一个可以线性存放 primitive type 数据的容器接口。 Buffer 主要包含了与类型( byte, char… )无关的功能。值得 注意的是 Buffer 及其子类都不是线程安全的。
每个 Buffer 都有以下的属性:
capacity
这个 Buffer 最多能放多少数据。 capacity 一般在 buffer 被创建的时候指定。
limit
在 Buffer 上进行的读写操作都不能越过这个下标。当写数据到 buffer 中时, limit 一般和 capacity 相等,当读数据时, limit 代表 buffer 中有效数据的长度。
position
读 / 写操作的当前下标。当使用 buffer 的相对位置进行读 / 写操作时,读 / 写会从这个下标进行,并在操作完成后, buffer 会更新下标的值。
mark
一个临时存放的位置下标。调用 mark() 会将 mark 设为当前的 position 的值,以后调用 reset() 会将 position 属性设置为 mark 的值。 mark 的值总是小于等于 position 的值,如果将 position 的值设的比 mark 小,当前的 mark 值会被抛弃掉。
这些属性总是满足以下条件:
0 <= mark <= position <= limit <= capacity
limit 和 position 的值除了通过 limit() 和 position() 函数来设置,也可以通 过下面这些函数来改变:
Buffer clear()
把 position 设为 0 ,把 limit 设为 capacity ,一般在把数据写入 Buffer 前调用。
Buffer flip()
把 limit 设为当前 position ,把 position 设为 0 ,一般在从 Buffer 读出数据前调用。
Buffer rewind()
把 position 设为 0 , limit 不变,一般在把数据重写入 Buffer 前调用。
Buffer 对象有可能是只读的,这时,任何 对该对象的写操作都会触发一个 ReadOnlyBufferException 。 isReadOnly() 方法可以用来判断一个 Buffer 是否只读。
ByteBuffer
在 Buffer 的子类中, ByteBuffer 是一个地位较为特殊的类,因为在 java.io.channels 中定义的各种 channel 的 IO 操作基本上都是围绕 ByteBuffer 展开的。
ByteBuffer 定义了 4 个 static 方法来做创建工作:
ByteBuffer allocate(int capacity)
创建一个指定 capacity 的 ByteBuffer 。
ByteBuffer allocateDirect(int capacity)
创建一个 direct 的 ByteBuffer ,这样的 ByteBuffer 在参与 IO 操作时性能会更好(很有可能是在底层的实现使用了 DMA 技术),相应的,创建和回收 direct 的 ByteBuffer 的代价也会高一些。 isDirect() 方法可以检查一个 buffer 是否是 direct 的。
ByteBuffer wrap(byte [] array)
ByteBuffer wrap(byte [] array, int offset, int length)
把一个 byte 数组或 byte 数组的一部分包装成 ByteBuffer 。
ByteBuffer 定义了一系列 get 和 put 操作来从中读写 byte 数据,如下面几个:
byte get()
ByteBuffer get(byte [] dst)
byte get(int index)
ByteBuffer put(byte b)
ByteBuffer put(byte [] src)
ByteBuffer put(int index, byte b)
这些操作可分为绝对定位和相对定为两种,相对定位的读写操作依靠 position 来定位 Buffer 中的位置,并在操作完成后会更 新 position 的 值。
在其它类型的 buffer 中,也定义了相同的函数来读写数据,唯一不同的就是一些参数和返回值的类型。
除了读写 byte 类型数据的函数, ByteBuffer 的一个特别之处是它还定义了读写其它 primitive 数据的方法,如:
int getInt()
从 ByteBuffer 中读出一个 int 值。
ByteBuffer putInt(int value)
写入一个 int 值到 ByteBuffer 中。
读写其它类型的数据牵涉到字节序问题, ByteBuffer 会按其字节序(大字节序或小字节序)写入或读出一个其它类型的数据( int,long… )。字节序可以用 order 方法来取得和设置:
ByteOrder order()
返回 ByteBuffer 的字节序。
ByteBuffer order(ByteOrder bo)
设置 ByteBuffer 的字节序。
ByteBuffer 另一个特别的地方是可以 在它的基础上得到其它类型的 buffer 。如:
CharBuffer asCharBuffer()
为当前的 ByteBuffer 创建一个 CharBuffer 的视图。在该视图 buffer 中的读写操作会按照 ByteBuffer 的字节序作用到 ByteBuffer 中的数据上。
用这类方法创建出来的 buffer 会从 ByteBuffer 的 position 位置开始到 limit 位置结束,可以看作是这段数据的视图。视图 buffer 的 readOnly 属性和 direct 属性与 ByteBuffer 的一致,而且也只有通过这种方法,才可以得到其他数据类型的 direct buffer 。
ByteOrder
用来表示 ByteBuffer 字节序的类,可将其看成 java 中的 enum 类型。主要定义了下面几个 static 方法和属性:
ByteOrder BIG_ENDIAN
代表大字节序的 ByteOrder 。
ByteOrder LITTLE_ENDIAN
代表小字节序的 ByteOrder 。
ByteOrder nativeOrder()
返回当前硬件平台的字节序。
MappedByteBuffer
ByteBuffer 的子类,是文件内容在内 存中的映射。这个类的实例需要通过 FileChannel 的 map() 方法来创建。
接下来看看一个使用 ByteBuffer 的例子,这个例子从标准输入不停地读入字符,当读满一行后,将收集的字符写到标准输出:
public static void main(String [] args) throws IOException { // 创建一个 capacity 为 256 的 ByteBuffer ByteBuffer buf = ByteBuffer.allocate(256); while ( true ) { // 从标准输入流读入一个字符 int c = System.in.read(); // 当读到输入流结束时,退出循环 if (c == -1) break ;
// 把读入的字符写入 ByteBuffer 中 buf.put(( byte ) c); // 当读完一行时,输出收集的字符 if (c == '\n' ) { // 调用 flip() 使 limit 变为当前的 position 的值 ,position 变为 0, // 为接下来从 ByteBuffer 读取做准备 buf.flip(); // 构建一个 byte 数组 byte [] content = new byte [buf.limit()]; // 从 ByteBuffer 中读取数据到 byte 数组中 buf.get(content); // 把 byte 数组的内容写到标准输出 System.out.print( new String(content)); // 调用 clear() 使 position 变为 0,limit 变为 capacity 的值, // 为接下来写入数据到 ByteBuffer 中做准备 buf.clear(); } } } |
Package java.nio.channels
这个包定义了 Channel 的概念, Channel 表现了一个可以进行 IO 操作的通道(比如,通过 FileChannel ,我们可以对文件进行读写操作)。 java.nio.channels 包含了文件系统和网络通讯相关的 channel 类。这个包通过 Selector 和 SelectableChannel 这两个类,还定义了一个进行非阻塞( non-blocking ) IO 操作的 API ,这对需要高性能 IO 的应用非常重要。
下面这张 UML 类图描述了 java.nio.channels 中 interface 的关系:
Channel
Channel 表现了一个可以进行 IO 操作的通道,该 interface 定义了以下方法:
boolean isOpen()
该 Channel 是否是打开的。
void close()
关闭这个 Channel ,相关的资源会被释放。
ReadableByteChannel
定义了一个可从中读取 byte 数据的 channel interface 。
int read(ByteBuffer dst)
从 channel 中读取 byte 数据并写到 ByteBuffer 中。返回读取的 byte 数。
WritableByteChannel
定义了一个可向其写 byte 数据的 channel interface 。
int write(ByteBuffer src)
从 ByteBuffer 中读取 byte 数据并写到 channel 中。返回写出的 byte 数。
ByteChannel
ByteChannel 并没有定义新的方法, 它的作用只是把 ReadableByteChannel 和 WritableByteChannel 合并在一起。
ScatteringByteChannel
继承了 ReadableByteChannel 并提供了同时往几个 ByteBuffer 中写数据的能力。
GatheringByteChannel
继承了 WritableByteChannel 并提供了同时从几个 ByteBuffer 中读数据的能力。
InterruptibleChannel
用来表现一个可以被异步关闭的 Channel 。这表现在两方面:
1. 当一个 InterruptibleChannel 的 close() 方法被调用时,其它 block 在这个 InterruptibleChannel 的 IO 操作上的线程会接收到一个 AsynchronousCloseException 。
2. 当一个 线程 block 在 InterruptibleChannel 的 IO 操作上时,另一个线程调用该线程的 interrupt() 方法会导致 channel 被关闭,该线程收到一个 ClosedByInterruptException ,同时线程的 interrupt 状态会被设置。
接下来的这张 UML 类图描述了 java.nio.channels 中类的关系:
非阻塞 IO
非阻塞 IO 的支持可以算是 NIO API 中最重要的功能,非阻塞 IO 允许应用程序同时监控多个 channel 以提高性能,这一功能是通过 Selector , SelectableChannel 和 SelectionKey 这 3 个类来实现的。
SelectableChannel 代表了可 以支持非阻塞 IO 操 作的 channel , 可以将其注册在 Selector 上,这种注册的关系由 SelectionKey 这个类来表现(见 UML 图)。 Selector 这个类通过 select() 函数,给应用程序提供了一个可以同时监控多个 IO channel 的方法:
应用程序通过调用 select() 函数,让 Selector 监控注册在其上的多个 SelectableChannel ,当有 channel 的 IO 操作可以进行时, select() 方法就会返回以让应用程序检查 channel 的状态,并作相应的处理。
下面是 JDK 1.4 中非阻塞 IO 的一个例子,这段 code 使用了非阻塞 IO 实现了一个 time server :
private static void acceptConnections( int port) throws Exception { // 打开一个 Selector Selector acceptSelector = SelectorProvider.provider().openSelector();
// 创建一个 ServerSocketChannel ,这是一个 SelectableChannel 的子类 ServerSocketChannel ssc = ServerSocketChannel.open(); // 将其设为 non-blocking 状态,这样才能进行非阻塞 IO 操作 ssc.configureBlocking( false );
margin: 0cm 0cm 0pt; text-align: le 发表评论
最新评论
|
评论