# Java 通道教程 – NIO 2.0

> 原文： [https://howtodoinjava.com/java7/nio/java-nio-2-0-channels/](https://howtodoinjava.com/java7/nio/java-nio-2-0-channels/)

通道是继[**缓冲区**](//howtodoinjava.com/java-7/nio/java-nio-2-0-working-with-buffers/ "Java NIO 2.0 : Working With Buffers")之后的`java.nio`的第二项重大创新，我们在我之前的教程中已详细了解到。 通道提供与 I/O 服务的直接连接。 **通道是一种在字节缓冲区和通道另一端的实体（通常是文件或套接字）之间有效传输数据的介质。** 通常，通道与操作系统文件描述符具有一对一的关系。 通道类提供了维持平台独立性所需的抽象，但仍可以对现代操作系统的本机 I/O 能力进行建模。 通道是网关，通过它可以以最小的开销访问操作系统的本机 I/O 服务，而缓冲区是通道用来发送和接收数据的内部端点。

## NIO 通道基础

在层次结构的顶部，有[`Channel`](https://docs.oracle.com/javase/7/docs/api/java/nio/channels/Channel.html "Channel")接口，如下所示：

```java
package java.nio.channels;

public interface Channel
{
	public boolean isOpen();
	public void close() throws IOException;
}

```

由于依赖底层平台的各种因素，`Channel`实现在操作系统之间存在根本性的差异，因此通道 API（或接口）仅描述了可以完成的工作。 **`Channel`实现通常使用本机代码执行实际工作。 通过这种方式，通道接口使您能够以受控且可移植的方式访问低级 I/O 服务。**

从顶级`Channel`接口可以看到，所有通道只有两个共同的操作：检查通道是否打开（`isOpen()`）和关闭打开的通道（`close()`）。

## 打开通道

众所周知，I/O 分为两大类：**文件 I/O 和流 I/O** 。 因此，通道有两种类型也就不足为奇了：文件和套接字。 `FileChannel`类和`SocketChannel`类用于处理这两个类别。

仅可以通过在打开的`RandomAccessFile`，`FileInputStream`或`FileOutputStream`对象上调用`getChannel()`方法来获得`FileChannel`对象。 您不能直接创建`FileChannel`对象。

```java
RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel();

```

与`FileChannel`相反，套接字通道具有工厂方法来直接创建新的套接字通道。 例如

```java
//How to open SocketChannel
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("somehost", someport));

//How to open ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind (new InetSocketAddress (somelocalport));

//How to open DatagramChannel
DatagramChannel dc = DatagramChannel.open();

```

上面的方法返回一个相应的套接字通道对象。 它们不是`RandomAccessFile.getChannel()`的新通道的来源。 如果已经存在，则它们返回与套接字关联的通道。 他们从不创造新的渠道。

## 使用通道

正如我们已经学习到缓冲区的教程一样，该教程介绍了通道与`ByteBuffer`对象之间的数据传输。 大多数读/写操作由从下面的接口实现的方法执行。

```java
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
{
}

```

**通道可以是单向或双向**。 给定的通道类可能实现`ReadableByteChannel`，它定义了`read()`方法。 另一个可能会实现`WritableByteChannel`以提供`write()`。 实现这些接口中的一个或另一个的类是单向的：它只能在一个方向上传输数据。 如果一个类实现两个接口（或扩展两个接口的`ByteChannel`），则它是双向的，可以双向传输数据。

如果您查看通道类，则会发现每个文件和套接字通道都实现了这三个接口。 就类定义而言，这意味着所有文件和套接字通道对象都是双向的。 对于套接字来说，这不是问题，因为它们始终是双向的，但是对于文件来说，这是一个问题。 从`FileInputStream`对象的`getChannel()`方法获得的`FileChannel`对象是只读的，但是在接口声明方面是双向的，因为 **`FileChannel`实现了`ByteChannel`** 。 在这样的通道上调用`write()`会抛出未选中的`NonWritableChannelException`，因为`FileInputStream`总是打开具有只读权限的文件。 因此，请记住，当通道连接到特定的 I/O 服务时，通道实例的特性将受到与其连接的服务的特性的限制。 连接到只读文件的`Channel`实例无法写入，即使该`Channel`实例所属的类可能具有`write()`方法。 程序员要知道如何打开通道，而不要尝试执行底层 I/O 服务不允许的操作。

```java
FileInputStream input = new FileInputStream ("readOnlyFile.txt");
FileChannel channel = input.getChannel();

// This will compile but will throw an IOException 
// because the underlying file is read-only
channel.write (buffer);

```

`ByteChannel`的`read()`和`write()`方法将`ByteBuffer`对象作为参数。 每个都返回传输的字节数，该字节数可以小于缓冲区中的字节数，甚至为零。 缓冲区的位置将增加相同的量。 如果执行了部分传输，则可以将缓冲区重新提交给通道以继续传输数据，并从该处停止传输。 重复直到缓冲区的`hasRemaining()`方法返回`false`。

在下面的示例中，我们将数据从一个通道复制到另一个通道（*或从一个文件复制到另一个文件*）：

```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

public class ChannelCopyExample
{
	public static void main(String args[]) throws IOException 
	{
		FileInputStream input = new FileInputStream ("testIn.txt");
		ReadableByteChannel source = input.getChannel();

		FileOutputStream output = new FileOutputStream ("testOut.txt");
		WritableByteChannel dest = output.getChannel();

		copyData(source, dest);

		source.close();
		dest.close();
	}

	private static void copyData(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();
		}
	}
}

```

**通道可以在阻塞或非阻塞模式下运行。** 处于非阻塞模式的通道永远不会使调用线程进入睡眠状态。 所请求的操作要么立即完成，要么返回结果表明未完成任何操作。 只能将面向流的通道（例如套接字和管道）置于非阻塞模式。

## 关闭通道

要关闭通道，请使用其`close()`方法。 与缓冲区不同，**通道在关闭**后不能重新使用。 开放通道表示与特定 I/O 服务的特定连接，并封装了该连接的状态。 关闭通道后，该连接将丢失，并且该通道不再连接任何东西。

**在通道上多次调用`close()`是没有害处的。 随后在关闭通道上对`close()`的调用无济于事，并立即返回。**

可以想到，套接字通道可能需要大量时间才能关闭，具体取决于系统的网络实现。 某些网络协议栈可能会在输出耗尽时阻止关闭。

通道的打开状态可以使用`isOpen()`方法进行测试。 如果返回`true`，则可以使用该通道。 如果为`false`，则该通道已关闭，无法再使用。 尝试读取，写入或执行任何其他需要通道处于打开状态的操作将导致`ClosedChannelException`。

**祝您学习愉快！**