首先,我们来更近距离地看一下基本的 Channel 接口。
Channel 接口的完整源码:
与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。你可以从顶层的 Channel 接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开(isOpen())和关闭一个打开的通道(close())。
从 Channel 的类层次关系上可以看出,有两个类并不位于 java.nio.channels 包中。这两个类是 AbstractInterruptibleChannel 和 AbstractSelectableChannel,位于 java.nio.channel.spi,它们分别为可中断的(interruptible)和可选择的(selectable)的通道实现提供所需的常用方法。尽管描述通道行为的接口都是在 java.nio.channels 包中定义的,不过具体的通道实现却都是从 java.nio.channels.spi 中的类引申来的。这使得他们可以访问受保护的方法,而这些方法普通的通道用户永远都不会调用。作为通道的一个使用者,你可以放心地忽视 SPI 包中包含的中间类。
从 Channel 接口引申出的其他接口都是面向字节的子接口,包括 WritableByteChannel 和 ReadableByteChannel。这也正好印证了通道只能在字节缓冲区上操作。同时也表明其它数据类型的通道也可以从 Channel 接口引申而来。这是一种很好的类设计,不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层 I/O 接口的。
◇ 打开通道
通道是访问 I/O 服务的导管。I/O 可以分为广义的两大类别:File I/O 和 Stream I/O,那么就对应于文件(file)通道和套接字(socket)通道。从通道类层次关系我们会发现有一个 FileChannel 类和三个 socket 通道类:SocketChannel、ServerSocketChannel 和 DatagramChannel。 创建并打开通道的一些示例:
java.net 的 socket 类也有新的 getChannel()方法。这些方法虽然能返回一个相应的 socket 通道对象,但它们却并非新通道的来源,RandomAccessFile.getChannel() 方法才是。只有在已经有通道存在的时候,它们才返回与一个 socket 关联的通道,它们永远不会创建新通道。
通道可以以多种方式创建。Socket 通道有可以直接创建新 socket 通道的工厂方法,但是一个 FileChannel 对象却只能通过在一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream 对象上调用 getChannel() 方法来获取。你不能直接创建一个 FileChannel 对象。
◇ 使用通道
在 NIO - Buffer 这篇文章中,我们了解到,通道将数据传输给 ByteBuffer 对象或者从 ByteBuffer 对象获取数据进行传输。ReadableByteChannel,通道是可读的,因此一定是从 Channel 读。读到哪里?读到 ByteBuffer;WritableByteChannel,通道是可写的,因此一定是写入 channel。写的内容来自哪里?来自 ByteBuffer。 看看关于通道的几个子接口:
通道可以是单向(unidirectional)或者双向的(bidirectional)。一个 channel 类可能实现定义 read() 方法的 ReadableByteChannel 接口,而另一个 channel 类也许实现 WritableByteChannel 接口以提供 write() 方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。
一个文件可以在不同的时候以不同的权限打开。从 FileInputStream 对象的 getChannel() 方法获取的 FileChannel 对象是只读的,不过从接口声明的角度来看却是双向的,因为 FileChannel 实现 ByteChannel 接口。在这样一个通道上调用 write() 方法将抛出未经检查的 NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件。
通道会连接一个特定 I/O 服务且通道实例(channel instance)的性能受它所连接的 I/O 服务的特征限制。一个连接到只读文件的 Channel 实例不能进行写操作,即使该实例所属的类可能有 write() 方法。因此,程序员需要知道通道是如何打开的,避免试图尝试一个底层 I/O 服务不允许的操作。 避免尝试底层不支持的操作:
◇ 关闭通道
与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定 I/O 服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。
调用通道的 close() 方法时,可能会导致在通道关闭底层 I/O 服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)高度取决于操作系统或者文件系统的。
在一个通道上多次调用 close() 方法是没有坏处的,但是如果第一个线程在 close() 方法中阻塞,那么在它完成关闭通道之前,任何其他调用 close() 方法都会阻塞。后续在该已关闭的通道上调用 close() 不会产生任何操作,只会立即返回。
可以通过 isOpen() 方法来测试通道的开放状态。如果返回 true 值,那么该通道可以使用。如果返回 false 值,那么该通道已关闭,不能再被使用。尝试进行任何需要通道处于开放状态作为前提的操作,如读、写等都会导致 ClosedChannelException 异常。
◇ Scatter/Gather
通道提供了一种被称为 Scatter/Gather 的重要新功能(有时也被称为矢量 I/O)。Scatter/Gather 是一个简单却强大的概念,它是指在多个缓冲区上实现一个简单的 I/O 操作。
对于一个 write 操作而言,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。缓冲区本身并不需要具备这种 gather 的能力(通常它们也没有此能力)。该 gather 过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。
对于 read 操作而言,从通道读取的数据会按顺序被分散(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。
大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。当你在一个通道上请求一个 Scatter/Gather 操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather 应该使用直接的 ByteBuffers 以从本地 I/O 获取最大性能优势。 scatter 描述如何扩展读操作,gather 描述如何扩展写操作:
数据从缓冲区阵列引用的每个缓冲区中 gather 并被组合成沿着通道发送的字节流。
从四个缓冲区 gather 后的数据写入通道
从通道传输来的数据被 scatter 到所列缓冲区,依次填充每个缓冲区(从缓冲区的 position 处开始到 limit 处结束)。这里显示的 position 和 limit 值是读(从 channel 读,即写入 buffer)操作开始之前的。
从通道读数据 scatter 到四个缓冲区
带 offset 和 length 参数版本的 read() 和 write() 方法使得我们可以使用缓冲区阵列的子集缓冲区。这里的 offset 值指哪个缓冲区将开始被使用,而不是指数据的 offset。这里的 length 参数指示要使用的缓冲区数量。 offset 和 length 意指从哪个缓冲区开始使用,并使用多少个缓冲区:
使用得当的话,Scatter/Gather 会是一个极其强大的工具。它允许你委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了你来回移动数据的工作,也就避免了缓冲区拷贝和减少了你需要编写、调试的代码数量。
- package java.nio.channels;
- public interface Channel {
- public boolean isOpen();
- public void close() throws IOException;
- }
与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。你可以从顶层的 Channel 接口看到,对所有通道来说只有两种共同的操作:检查一个通道是否打开(isOpen())和关闭一个打开的通道(close())。
从 Channel 的类层次关系上可以看出,有两个类并不位于 java.nio.channels 包中。这两个类是 AbstractInterruptibleChannel 和 AbstractSelectableChannel,位于 java.nio.channel.spi,它们分别为可中断的(interruptible)和可选择的(selectable)的通道实现提供所需的常用方法。尽管描述通道行为的接口都是在 java.nio.channels 包中定义的,不过具体的通道实现却都是从 java.nio.channels.spi 中的类引申来的。这使得他们可以访问受保护的方法,而这些方法普通的通道用户永远都不会调用。作为通道的一个使用者,你可以放心地忽视 SPI 包中包含的中间类。
从 Channel 接口引申出的其他接口都是面向字节的子接口,包括 WritableByteChannel 和 ReadableByteChannel。这也正好印证了通道只能在字节缓冲区上操作。同时也表明其它数据类型的通道也可以从 Channel 接口引申而来。这是一种很好的类设计,不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层 I/O 接口的。
◇ 打开通道
通道是访问 I/O 服务的导管。I/O 可以分为广义的两大类别:File I/O 和 Stream I/O,那么就对应于文件(file)通道和套接字(socket)通道。从通道类层次关系我们会发现有一个 FileChannel 类和三个 socket 通道类:SocketChannel、ServerSocketChannel 和 DatagramChannel。 创建并打开通道的一些示例:
- SocketChannel sc = SocketChannel.open();
- sc.connect (new InetSocketAddress ("localhost", serverPort));
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ssc.socket().bind (new InetSocketAddress(serverPort));
- DatagramChannel dc = DatagramChannel.open();
- RandomAccessFile raf = new RandomAccessFile("somefile", "r");
- FileChannel fc = raf.getChannel();
java.net 的 socket 类也有新的 getChannel()方法。这些方法虽然能返回一个相应的 socket 通道对象,但它们却并非新通道的来源,RandomAccessFile.getChannel() 方法才是。只有在已经有通道存在的时候,它们才返回与一个 socket 关联的通道,它们永远不会创建新通道。
通道可以以多种方式创建。Socket 通道有可以直接创建新 socket 通道的工厂方法,但是一个 FileChannel 对象却只能通过在一个打开的 RandomAccessFile、FileInputStream 或 FileOutputStream 对象上调用 getChannel() 方法来获取。你不能直接创建一个 FileChannel 对象。
◇ 使用通道
在 NIO - Buffer 这篇文章中,我们了解到,通道将数据传输给 ByteBuffer 对象或者从 ByteBuffer 对象获取数据进行传输。ReadableByteChannel,通道是可读的,因此一定是从 Channel 读。读到哪里?读到 ByteBuffer;WritableByteChannel,通道是可写的,因此一定是写入 channel。写的内容来自哪里?来自 ByteBuffer。 看看关于通道的几个子接口:
- public interface ReadableByteChannel extends Channel {
- // Reads a sequence of bytes from this channel into the given buffer.
- public int read(ByteBuffer dst) throws IOException;
- }
- public interface WritableByteChannel extends Channel {
- // Writes a sequence of bytes to this channel from the given buffer.
- public int write(ByteBuffer src) throws IOException;
- }
- public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {}
通道可以是单向(unidirectional)或者双向的(bidirectional)。一个 channel 类可能实现定义 read() 方法的 ReadableByteChannel 接口,而另一个 channel 类也许实现 WritableByteChannel 接口以提供 write() 方法。实现这两种接口其中之一的类都是单向的,只能在一个方向上传输数据。如果一个类同时实现这两个接口,那么它是双向的,可以双向传输数据。
一个文件可以在不同的时候以不同的权限打开。从 FileInputStream 对象的 getChannel() 方法获取的 FileChannel 对象是只读的,不过从接口声明的角度来看却是双向的,因为 FileChannel 实现 ByteChannel 接口。在这样一个通道上调用 write() 方法将抛出未经检查的 NonWritableChannelException 异常,因为 FileInputStream 对象总是以 read-only 的权限打开文件。
通道会连接一个特定 I/O 服务且通道实例(channel instance)的性能受它所连接的 I/O 服务的特征限制。一个连接到只读文件的 Channel 实例不能进行写操作,即使该实例所属的类可能有 write() 方法。因此,程序员需要知道通道是如何打开的,避免试图尝试一个底层 I/O 服务不允许的操作。 避免尝试底层不支持的操作:
- // A ByteBuffer named buffer contains data to be written
- FileInputStream input = new FileInputStream (fileName);
- FileChannel channel = input.getChannel();
- // This will compile but will throw an IOException for the underlying file is read-only
- channel.write(buffer);
◇ 关闭通道
与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定 I/O 服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。
调用通道的 close() 方法时,可能会导致在通道关闭底层 I/O 服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)高度取决于操作系统或者文件系统的。
在一个通道上多次调用 close() 方法是没有坏处的,但是如果第一个线程在 close() 方法中阻塞,那么在它完成关闭通道之前,任何其他调用 close() 方法都会阻塞。后续在该已关闭的通道上调用 close() 不会产生任何操作,只会立即返回。
可以通过 isOpen() 方法来测试通道的开放状态。如果返回 true 值,那么该通道可以使用。如果返回 false 值,那么该通道已关闭,不能再被使用。尝试进行任何需要通道处于开放状态作为前提的操作,如读、写等都会导致 ClosedChannelException 异常。
◇ Scatter/Gather
通道提供了一种被称为 Scatter/Gather 的重要新功能(有时也被称为矢量 I/O)。Scatter/Gather 是一个简单却强大的概念,它是指在多个缓冲区上实现一个简单的 I/O 操作。
对于一个 write 操作而言,数据是从几个缓冲区按顺序抽取(称为 gather)并沿着通道发送的。缓冲区本身并不需要具备这种 gather 的能力(通常它们也没有此能力)。该 gather 过程的效果就好比全部缓冲区的内容被连结起来,并在发送数据前存放到一个大的缓冲区中。
对于 read 操作而言,从通道读取的数据会按顺序被分散(称为 scatter)到多个缓冲区,将每个缓冲区填满直至通道中的数据或者缓冲区的最大空间被消耗完。
大多数现代操作系统都支持本地矢量 I/O(native vectored I/O)。当你在一个通道上请求一个 Scatter/Gather 操作时,该请求会被翻译为适当的本地调用来直接填充或抽取缓冲区。这是一个很大的进步,因为减少或避免了缓冲区拷贝和系统调用。Scatter/Gather 应该使用直接的 ByteBuffers 以从本地 I/O 获取最大性能优势。 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;
- }
数据从缓冲区阵列引用的每个缓冲区中 gather 并被组合成沿着通道发送的字节流。
从四个缓冲区 gather 后的数据写入通道
从通道传输来的数据被 scatter 到所列缓冲区,依次填充每个缓冲区(从缓冲区的 position 处开始到 limit 处结束)。这里显示的 position 和 limit 值是读(从 channel 读,即写入 buffer)操作开始之前的。
从通道读数据 scatter 到四个缓冲区
带 offset 和 length 参数版本的 read() 和 write() 方法使得我们可以使用缓冲区阵列的子集缓冲区。这里的 offset 值指哪个缓冲区将开始被使用,而不是指数据的 offset。这里的 length 参数指示要使用的缓冲区数量。 offset 和 length 意指从哪个缓冲区开始使用,并使用多少个缓冲区:
- // 假设我们有一个五元素的 fiveBuffers 阵列,它已经被初始化并引用了五个缓冲区
- ByteBuffer[] fiveBuffers = ...
- // 下面的代码将会将第二个、第三个和第四个缓冲区的内容写入通道
- int bytesRead = channel.write(fiveBuffers, 1, 3);
使用得当的话,Scatter/Gather 会是一个极其强大的工具。它允许你委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了你来回移动数据的工作,也就避免了缓冲区拷贝和减少了你需要编写、调试的代码数量。