• J3 - 白起
  • 技术(NIO # 通道 # Channel)


这是 IO 相关的第三篇​通道​,主要讲解一下通道是什么,在 Java NIO 中的体系及使用。能被称为 NIO 中的三大组件之一作用肯定是不言而喻的,所以对于通道的掌握还是很重要的,那我们往下看把!

以下内容“通道”一词就是“Channel”,只是我更喜欢用中文表示而已,仅我个人喜好,并无它意。

一、什么是通道

百度词贴:

详解,NIO中的通道(Channel)_输出流

从百度解释来看:通道主要用来传输数据的一条道路。

​而在 NIO 中,通道的作用也是如此:​传输数据​,将“原缓冲区”与“目标缓冲区”要交换的数据进行传输​。

很明显通道是作用与缓冲区的,所以读了上篇本人写得《详解,NIO中的缓冲区》那么我们得出下面这张图:

详解,NIO中的通道(Channel)_输出流_02

二、NIO中的通道体系

在 IDEA 中我观察 Channel 的类继承关系时,发现好复杂呀,我点进源码看它的继承接口和实现类,发现超复杂,最后还是放弃通过 IDEA 看了。

所以我去看了 JDK8 的 API 文档找出了 Channel 的相关信息,如下。

1、​父接口


  • AutoCloseable:自动关闭流,而不需要显式地调用 close ()方法。
  • Closeable:关闭 IO 流,释放系统资源。

2、​直接子接口


  • AsynchronousByteChannel:支持异步 IO 操作,单位时字节。
  • AsynchronousChannel:支持异步 IO 操作。
  • ByteChannel:继承 ReadableByteChannel 和 WritableByteChannel 接口允许对baty进行读写操作。
  • GatheringByteChannel:使接口可以将多个缓冲区中的数据写入通道。
  • InterruptibleChannel:使通道能以异步的方式进行关闭与中断。
  • MulticastChannel:使通道支持一个多播的功能,可以理解同时向多个主机发送数据。
  • NetworkChannel:主要作用是使通道与 Socket 进行关联,是通道中的数据能在 Socket 技术上进行传输。
  • ReadableByteChannel:是通道允许对字节进行读操作。
  • ScatteringByteChannel:主要作用时可以从通道中读取字节到多个缓冲区中。
  • SeekableByteChannel:主要作用是在字节通道中维护 position ,以及允许 position 发生改变。
  • WritableByteChannel:使通道允许对字节进行写操作。

3、​所有已知实现类


  • AbstractInterruptibleChannel:提供一个可以被中断的通道基本实现。
  • AbstractSelectableChannel:可选通道的基本实现,该类定义了处理通道注册、注销和关闭机制的方法。
  • AsynchronousFileChannel:可以以异步的方式从文件读取或往文件写入数据。
  • AsynchronousServerSocketChannel:用于面向流的服务端的异步通道。
  • AsynchronousSocketChannel:用于面向流的客户端的异步通道。
  • DatagramChannel:面向无连接的套接字的可选通道。
  • FileChannel​:继承 AbstractInterruptibleChannel 类,主要作用时读取、写入、映射和操作文件的通道。该通道永远是阻塞的操作。
  • Pipe.SinkChannel:一个代表​​Pipe​​的可写端的通道。
  • Pipe.SourceChannel:一个代表​​Pipe​​的可读端的通道。
  • SelectableChannel:可通过​​Selector​​复用的通道。
  • ServerSocketChannel​:面向连接的服务端通道。
  • SocketChannel​:面向连接的客户端通道。

Channel 体系确实很庞大,所以我们不需要全部的去深入它们,只需要知道其中的几个就行,比如:​​FileChannel​​​、​​ServerSocketChannel​​​、​​SocketChannel​​等。

那下面就先看看 ​​FileChannel​​ 也是用的最多的一个其它的下次介绍。

三、FileChannel 类使用

先看类结构图:

详解,NIO中的通道(Channel)_开发语言_03

再看 API 图:

详解,NIO中的通道(Channel)_开发语言_04

一、获取文件通道及读取操作

方式一:

FileChannel fileChannel = FileChannel.open(new File("src/channel/j3.txt").toPath(), StandardOpenOption.WRITE, StandardOpenOption.READ);

解释:根据一个指定的文件获取一个可读写的文件通道

​StandardOpenOption​​ 枚举可以指定通道的读写权限。

public enum StandardOpenOption implements OpenOption {
READ, // 读
WRITE, // 写
APPEND, // 在写模式下,进行追加写
TRUNCATE_EXISTING, // 如果文件已经存在,并且它被打开以进行WRITE访问,那么它的长度将被截断为0。如果文件仅以READ访问方式打开,则忽略此选项。
CREATE, // 如果文件不存在,请创建一个新文件。如果还设置了CREATE_NEW选项,则忽略此选项。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
CREATE_NEW, // 创建一个新文件,如果文件已经存在则失败。与其他文件系统操作相比,检查文件是否存在以及创建文件(如果不存在)是原子性的。
DELETE_ON_CLOSE, // 关闭时删除文件
SPARSE, // 稀疏文件。当与CREATE_NEW选项一起使用时,此选项将提示新文件将是稀疏的。当文件系统不支持创建稀疏文件时,该选项将被忽略。
SYNC, // 要求对文件内容或元数据的每次更新都以同步方式写入底层存储设备。
DSYNC; // 要求对文件内容的每次更新都以同步方式写入底层存储设备。
}

案例:

@Test
public void channelTest() throws IOException {
// 获得一个根据指定文件路径的读写权限文件通道
FileChannel fileChannel = FileChannel.open(new File("src/channel/j3.txt").toPath(), StandardOpenOption.WRITE, StandardOpenOption.READ);
// 获得一段有指定内容的缓冲区
ByteBuffer source = ByteBuffer.wrap("HelloWorld,J3-baiqi".getBytes(StandardCharsets.UTF_8));
// 空的缓冲区
ByteBuffer target = ByteBuffer.allocate(50);
log.info("fileChannel.position():{}", fileChannel.position());
// 将缓冲区中的内容写入文件通道
fileChannel.write(source);
// 通道大小
log.info("fileChannel.position():{}", fileChannel.position());
// 设置读写位置
fileChannel.position(0);
// 将通道中的内容写到空缓冲区
fileChannel.read(target);
// 转换缓冲区读写模式
target.flip();
log.info("target:,{}", new String(target.array(), 0, target.limit()));
//关闭资源
fileChannel.close();
}

方式二:

FileInputStream fileInputStream = new FileInputStream("src/channel/j3.txt");
FileChannel channel = fileInputStream.getChannel();

解释:根据一个文件流获取对应的文件通道,通道的读写权限由流的输入输出决定。

输入 ==》读

输出 ==》写

案例:

@Test
public void channelTest02() throws IOException {
// 获取输出流
FileOutputStream outputStream = new FileOutputStream("src/channel/j3.txt");
// 根据输出流获得一个 “写” 权限的通道
FileChannel outChannel = outputStream.getChannel();
// 获得一个有指定内容的缓冲区
ByteBuffer source = ByteBuffer.wrap("HelloWorld,J3-baiqi".getBytes(StandardCharsets.UTF_8));
// 将缓冲区内容写入到通道
outChannel.write(source);

// ===============================================================

// 获取输入流
FileInputStream fileInputStream = new FileInputStream("src/channel/j3.txt");
// 根据输入流获得一个 “读” 权限的通道
FileChannel inChannel = fileInputStream.getChannel();
// 获得一个空内容的缓冲区
ByteBuffer target = ByteBuffer.allocate(50);
// 将通道中的内容读到缓冲区
inChannel.read(target);
// 转换缓冲区读写模式
target.flip();
// 读出缓冲区中的内容
log.info("target:,{}", new String(target.array(), 0, target.limit()));
//关闭资源
outChannel.close();
inChannel.close();
}

上面介绍的两个案例实现了通道的基本操作获取、读、写。如果细心的人可以发现一点非常别扭的地方就是通道的读与写的理解,​​write​​ 是写操作,但被 Channel 调用后就变成了将数据写入通道有点读取数据的意思,反之亦然。

对于这种别扭的地方,我们要如何区理解呢!我说说我的理解:


通道由空变成非空就是写,缓冲区向通道写入了数据;

通道由非空变成空就是读,缓冲区从通道读取了数据。


二、文件复制操作

下面介绍 FileChannel 中两个文件复制操作 API ,非常方便好用,在实际项目中也是有运用。


  • ​transferTo​​:将数据复制到目标对象中。
  • ​transferFrom​​:将数据从目标对象中复制给自己。

这两个 API 的作用一样,就是作用对象不一样,如果调用方是有数据的那就用 ​​transferTo​​​,反之则用 ​​transferFrom​​ 。

案例:

@Test
public void copyTest() throws IOException {
/*
需求:将一个视频文件从F:\\Channel\\a.mp4复制到F:\\Channel\\b.mp4
*/
// 准备输入流(源文件)
FileInputStream fileInputStream = new FileInputStream("F:\\Channel\\a.mp4");
// 准备输出流(目标文件)
FileOutputStream fileOutputStream = new FileOutputStream("F:\\Channel\\b.mp4");

// 根据流获取通道
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();

// 指向复制方法
// outputStreamChannel.transferFrom(inputStreamChannel, 0, inputStreamChannel.size());
inputStreamChannel.transferTo(0, inputStreamChannel.size(), outputStreamChannel);
// 关闭资源
fileInputStream.close();
fileOutputStream.close();
}

再来对比一下原生 BIO 复制文件操作:

@Test
public void copyTest2() throws IOException {
/*
需求:将一个视频文件从F:\\Channel\\a.mp4复制到F:\\Channel\\b.mp4
*/
// 准备输入流(源文件)
FileInputStream fileInputStream = new FileInputStream("F:\\Channel\\a.mp4");
// 准备输出流(目标文件)
FileOutputStream fileOutputStream = new FileOutputStream("F:\\Channel\\b.mp4");
//存储数据的字节数组
byte[] b = new byte[1024];
while (true) {
//从输入流中读取数据到字节数组中
int res = fileInputStream.read(b);
//判断是否读到文件末尾,是就跳出循环
if (res == -1) {
break;
}
//将字节数组中的数据通过输出流,写到目标文件中
fileOutputStream.write(b, 0, res);
}
fileInputStream.close();
fileOutputStream.close();
}

两者对比一下,是不是 NIO 的方式较为简单。

不过通过案例对比虽然 NIO FileChannel 方式操作较为简单,但是,在​效率上并不比 InputStream 或 OutputStream 高很多​,这是因为 NIO 的出现最主要的就是解决阻塞问题,通过 NIO 把线程变成非阻塞这样就提高效率。

而 NIO 的非阻塞与 Socket 相关的通道有关即网络 IO,这些后面会说,在这里只是提一嘴。

四、最后

通篇下来,难点基本上是没有的,主要就是理解 Channel 的作用:​传输数据的通道​。

然后就简单介绍了通道中用的比较多的文件通道(FileChannel)的基本使用,对于操作文件使用它还是比传统的方式简单的,至少在 API 方面是有体现,而我也做了相关案例。

那这就是 Channel 相关的内容了,虽然本片讲的简单了点,但毕竟不是专业 API 讲解,有时间还是建议看看通道中其他方法的使用。

​好了,今天的内容到这里就结束了,关注我,我们下期见​

查阅或参考资料:

《NIO与Socket编程指南》高洪严著



由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
感谢您的阅读,十分欢迎并感谢您的关注。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

​掘金:J3-白起​

​知乎:J3-白起​

这是一个技术一般,但热衷于分享;经验尚浅,但脸皮够厚;明明年轻有颜值,但非要靠才华吃饭的程序员。

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^