“人生苦短,不如养狗 作者:Brucebat.Sun ”
一、基本概念
在上一个章节中我们简单了解到Channel在NIO中的角色和Stream在IO中的角色有些类似,但是特性上却不尽相同。如果说Stream就像生活中的水流一样,那么Channel就如同交通中能够双向行驶的隧道。前者的数据流向固定,也就意味着同一个流只能完成读取或者写入其中一种操作,而后者则能够同时进行读取和写入两种操作。同时在数据传输过程中Channel也对Stream进行了进一步的改进处理,Channel中是通过buffer(也即缓冲区)来进行数据的装载和传输,而Stream中则直接以字节为单位进行数据的传输操作。
结合上面的描述,我们来对Channel和Stream的特性进行一个简单的对比:
Stream | Channel | |
---|---|---|
是否支持异步 | 否 | 是 |
数据传输方式 | 单工,仅支持读取或写入 | 全双工,支持同时进行读取和写入操作 |
是否依赖buffer | 否,即按照逐个字节传输 | 是,即按照逐个buffer传输 |
从上面的对比我们不难发现,相比传统IO,NIO在设计思路上更接近于底层操作系统的I/O实现方式,即使用缓冲区的方式来进行数据的批量装载和分组传输,这也是NIO在传输速度上优于传统IO的原因之一。但这种方式也意味着在实际使用Channel时,我们必须要依赖Buffer类来完成对于数据的读写,即如下展示:
代码语言:javascript复制读数据:
channel -> buffer -> program
写数据:
program -> buffer -> channel
二、基本使用
点开JDK提供的NIO包我们会看到一长串的Channel类列表,其中最为重要和常见的子类如下:
从上图中我们不难发现NIO提供了基于处理的数据不同分为两类Channel,一类是针对文件IO的Channel,一类是针对网络IO的Channel。需要注意的是,针对文件IO的Channel不能被设置成阻塞模式,同样也不支持多路复用模式,这一特性也符合操作系统对于纯文件类型数据的IO处理。下面笔者将会基于FileChannel
来简单说明一下针对文件IO的Channel的使用。
FileChannel
NIO类库为我们提供了如下三种方式来获取FileChannel
对象:
通过FileChannel自身提供了open方法获取
:通过这种方式可以明确指定打开通道的特定操作选项,如标准打开选项、处理符号链接的选项等。示例如下:
FileChannel channel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
通过FileInputStream/FileOutputStream获取
:通过这种方式获取到的通道只能以读或者写模式打开文件,对应读写模式是继承自对应的Stream对象。示例如下:
FileInputStream inputStream = new FileInputStream(new File("test.txt"));
// 获取一个只读的文件通道
FileChannel inChannel = inputStream.getChannel();
// ============= FileInputStream.getChannel() ======
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
// 这里如果初始化FileInputStream时没有设置channel则使用open方法初始化一个只读通道
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
通过RandomAccessFile获取
:通过这种方式获取的通道会继承RandomAccessFile对象设置的读写模式。示例如下:
RandomAccessFile file = new RandomAccessFile("test.txt", "rw");
// 获取一个可读可写的文件通道
FileChannel channel = file.getChannel();
// ============= RandomAccessFile.getChannel() ======
public final FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
// 区别于文件流这里在使用open方法初始化通道时默认是可读的,即当前文件处于只写模式时使用getChannel()获取到的通道也是可读可写的
channel = FileChannelImpl.open(fd, path, true, rw, this);
}
return channel;
}
}
在获取FileChannel对象之后,可以根据FileChannel提供的read()
或者write()
方法对文件进行读取或者写入操作。需要注意的是,上文提到过在实际使用读写方法时需要依赖缓冲区来完成对应,下面我们来分别看一下读数据和写数据的例子:
1. 读数据
代码语言:javascript复制FileChannel channel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(5);
while(channel.read(buf)!=-1){
buf.flip();
System.out.print(new String(buf.array()));
buf.clear();
}
channel.close();
2. 写数据
代码语言:javascript复制FileChannel channel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.WRITE);
ByteBuffer buf = ByteBuffer.allocate(20);
byte[] data = "Hello, world!".getBytes();
for (int i = 0; i < data.length; ) {
buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
buf.flip();
i = channel.write(buf);
buf.compact();
}
channel.force(false);
channel.close();
需要注意,在上面的实例代码中使用的都是FileChannel原生提供的获取方法,且在进行选项设置的时候只使用了读或者写模式,所以在进行文件的读写操作时都是从文件的头部开始的。当然具体的开始位置还取决于Buffer对象中初始position的设置。
总结
以上就是Channel类基本概念和针对文件IO的FileChannel基本使用的介绍,在后续的章节中我们会针对网络IO相关的Channel使用进行具体的学习和介绍。
疫情还在继续,希望大家注意防护,身体健康,保持每天好心情~~