1.简介

通道是 Java NIO 的核心内容之一,在使用上,通道需和缓存类(ByteBuffer)配合完成读写等操作。与传统的流式 IO 中数据单向流动不同,通道中的数据可以双向流动。通道既可以读,也可以写。这里我们举个例子说明一下,我们可以把通道看做水管,把缓存看做水塔,把文件看做水库,把水看做数据。当从磁盘中将文件数据读取到缓存中时,就是从水库向水塔里抽水。当然,从磁盘里读取数据并不会将读取的部分从磁盘里删除,但从水库里抽水,则水库里的水量在无补充的情况下确实变少了。当然,这只是一个小问题,大家不要扣这个细节哈,继续往下说。当水塔中存储了水之后,我们可以用这些水烧饭,浇花等,这就相当于处理缓存的数据。过了一段时间后,水塔需要进行清洗。这个时候需要把水塔里的水放回水库中,这就相当于向磁盘中写入数据。通过这里例子,大家应该知道通道是什么了,以及有什么用。既然知道了,那么我们继续往下看。

Java NIO 出现在 JDK 1.4 中,由于 NIO 效率高于传统的 IO,所以 Sun 公司从底层对传统 IO 的实现进行了修改。修改的方式就是在保证兼容性的情况下,使用 NIO 重构 IO 的方法实现,无形中提高了传统 IO 的效率。

2.基本操作

通道类型分为两种,一种是面向文件的,另一种是面向网络的。具体的类声明如下:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

正如上列表,NIO 通道涵盖了文件 IO,TCP 和 UDP 网络 IO 等通道类型。本文我们先来说说文件通道。

2.1 创建通道

FileChannel 是一个用于连接文件的通道,通过该通道,既可以从文件中读取,也可以向文件中写入数据。与SocketChannel 不同,FileChannel 无法设置为非阻塞模式,这意味着它只能运行在阻塞模式下。在使用FileChannel 之前,需要先打开它。由于 FileChannel 是一个抽象类,所以不能通过直接创建而来。必须通过像 InputStream、OutputStream 或 RandomAccessFile 等实例获取一个 FileChannel 实例。

FileInputStream fis = new FileInputStream(FILE_PATH);
FileChannel channel = fis.getChannel(); FileOutputStream fos = new FileOutputStream(FILE_PATH);
FileChannel channel = fis.getChannel(); RandomAccessFile raf = new RandomAccessFile(FILE_PATH , "rw");
FileChannel channel = raf.getChannel();

2.2 读写操作

读写操作比较简单,这里直接上代码了。下面的代码会先向文件中写入数据,然后再将写入的数据读出来并打印。代码如下:

// 获取管道
RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw");
FileChannel rafChannel = raf.getChannel(); // 准备数据
String data = "新数据,时间: " + System.currentTimeMillis();
System.out.println("原数据:\n" + " " + data);
ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.clear();
buffer.put(data.getBytes());
buffer.flip(); // 写入数据
rafChannel.write(buffer); rafChannel.close();
raf.close(); // 重新打开管道
raf = new RandomAccessFile(FILE_PATH, "rw");
rafChannel = raf.getChannel(); // 读取刚刚写入的数据
buffer.clear();
rafChannel.read(buffer); // 打印读取出的数据
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println("读取到的数据:\n" + " " + new String(bytes)); rafChannel.close();
raf.close();

上面的代码输出结果如下:

2.3 数据转移操作

我们有时需要将一个文件中的内容复制到另一个文件中去,最容易想到的做法是利用传统的 IO 将源文件中的内容读取到内存中,然后再往目标文件中写入。现在,有了 NIO,我们可以利用更方便快捷的方式去完成复制操作。FileChannel 提供了一对数据转移方法 - transferFrom/transferTo,通过使用这两个方法,即可简化文件复制操作。

public static void main(String[] args) throws IOException {
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel(); RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel(); long position = 0;
long count = fromChannel.size(); // 将 fromFile 文件找那个的数据转移到 toFile 中去
System.out.println("before transfer: " + readChannel(toChannel));
fromChannel.transferTo(position, count, toChannel);
System.out.println("after transfer : " + readChannel(toChannel)); fromChannel.close();
fromFile.close();
toChannel.close();
toFile.close();
} private static String readChannel(FileChannel channel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.clear(); // 将 channel 读取位置设为 0,也就是文件开始位置
channel.position(0);
channel.read(buffer); // 再次将文件位置归零
channel.position(0); buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
return new String(bytes);
}

通过上面的代码,我们可以明显感受到,利用 transferTo 减少了编码量。那么为什么利用 transferTo 可以减少编码量呢?在解答这个问题前,先来说说程序读取数据和写入文件的过程。

我们现在所使用的 PC 操作系统,将内存分为了内核空间和用户空间。操作系统的内核和一些硬件的驱动程序就是运行在内核空间内,而用户空间就是我们自己写的程序所能运行的内存区域。这里,当我们调用 read 从磁盘中读取数据时,内核会首先将数据读取到内核空间中,然后再将数据从内核空间复制到用户空间内。也就是说,我们需要通过内核进行数据中转。同样,写入数据也是如此。系统先从用户空间将数据拷贝到内核空间中,然后再由内核空间向磁盘写入。相关示意图如下:

与上面的数据流向不同,FileChannel 的 transferTo 方法底层基于 sendfile64(Linux 平台下)系统调用实现。sendfile64 会直接在内核空间内进行数据拷贝,免去了内核往用户空间拷贝,用户空间再往内核空间拷贝这两步操作,因此提高了效率。其示意图如下:

通过上面的讲解,大家应该知道了 transferTo 和 transferFrom 的效率会高于传统的 read 和 write 在效率上的区别。区别的原因在于免去了内核空间和用户空间的相互拷贝,虽然内存间拷贝的速度比较快,但涉及到大量的数据拷贝时,相互拷贝的带来的消耗是不应该被忽略的。

讲完了背景知识,咱们再来看看 FileChannel 是怎样调用 sendfile64 这个函数的。相关代码如下:

public long transferTo(long position, long count,
WritableByteChannel target)
throws IOException
{
// 省略一些代码 int icount = (int)Math.min(count, Integer.MAX_VALUE);
if ((sz - position) < icount)
icount = (int)(sz - position); long n; // Attempt a direct transfer, if the kernel supports it
if ((n = transferToDirectly(position, icount, target)) >= 0)
return n; // Attempt a mapped transfer, but only to trusted channel types
if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
return n; // Slow path for untrusted targets
return transferToArbitraryChannel(position, icount, target);
} private long transferToDirectly(long position, int icount,
WritableByteChannel target)
throws IOException
{
// 省略一些代码 long n = -1;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return -1;
do {
n = transferTo0(thisFDVal, position, icount, targetFDVal);
} while ((n == IOStatus.INTERRUPTED) && isOpen()); // 省略一些代码 return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end (n > -1);
}
}

从上面代码(transferToDirectly 方法可以在 openjdk/jdk/src/share/classes/sun/nio/ch/FileChannelImpl.java 中找到)中可以看得出 transferTo 的调用路径,先是调用 transferToDirectly,然后 transferToDirectly 再调用 transferTo0。transferTo0 是 native 类型的方法,我们再去看看 transferTo0 是怎样实现的,其代码在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c中。

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
jint srcFD,
jlong position, jlong count,
jint dstFD)
{
#if defined(__linux__)
off64_t offset = (off64_t)position; jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);
if (n < 0) {
if (errno == EAGAIN)
return IOS_UNAVAILABLE;
if ((errno == EINVAL) && ((ssize_t)count >= 0))
return IOS_UNSUPPORTED_CASE;
if (errno == EINTR) {
return IOS_INTERRUPTED;
}
JNU_ThrowIOExceptionWithLastError(env, "Transfer failed");
return IOS_THROWN;
}
return n; // 其他平台的代码省略
#endif
}

如上所示,transferTo0 最终调用了 sendfile64 函数,关于 sendfile64 这个系统调用的详细说明,请参考 man-page,这里就不展开说明了。

2.4 内存映射

内存映射这个概念源自操作系统,是指将一个文件映射到某一段虚拟内存(物理内存可能不连续)上去。我们通过对这段虚拟内存的读写即可达到对文件的读写的效果,从而可以简化对文件的操作。当然,这只是内存映射的一个优点。内存映射还有其他的一些优点,比如两个进程映射同一个文件,可以实现进程间通信。再比如,C 程序运行时需要 C 标准库支持,操作系统将 C 标准库放到了内存中,普通的 C 程序只需要将 C 标准库映射到自己的进程空间内就行了,从而可以降低内存占用。以上简单介绍了内存映射的概念及作用,关于这方面的知识,建议大家去看《深入理解计算机系统》关于内存映射的章节,讲的很好。

Unix/Linux 操作系统内存映射的系统调用mmap,Java 在这个系统调用的基础上,封装了 Java 的内存映射方法。这里我就不一步一步往下追踪了,大家有兴趣可以自己追踪一下 Java 封装的内存映射方法的调用栈。下面来简单的示例演示一下内存映射的用法:

// 从标准输入获取数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
String str = sc.nextLine();
byte[] bytes = str.getBytes(); RandomAccessFile raf = new RandomAccessFile("map.txt", "rw");
FileChannel channel = raf.getChannel(); // 获取内存映射缓冲区,并向缓冲区写入数据
MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length);
mappedBuffer.put(bytes); raf.close();
raf.close(); // 再次打开刚刚的文件,读取其中的内容
raf = new RandomAccessFile("map.txt", "rw");
channel = raf.getChannel();
System.out.println("\n文件内容:")
System.out.println(readChannel(channel)); raf.close();
raf.close();

上面的代码从标准输入中获取数据,然后将数据通过内存映射缓存写入到文件中。代码运行结果如下:

接下来在用 C 代码演示上面代码的功能,如下:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <memory.h>
#include <unistd.h> int main() {
int dstfd;
void *dst;
char buf[64], out[64];
int len; printf("Please input:\n");
scanf("%s", buf);
len = strlen(buf); // 打开文件
dstfd = open("dst.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
lseek(dstfd, len - 1, SEEK_SET);
write(dstfd, "", 1); // 将文件映射到内存中
dst = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, dstfd, 0); // 将输入的数据拷贝到映射内存中
memcpy(dst, buf, len); munmap(dst, len);
close(dstfd); // 重新打开文件,并输出文件内容
dstfd = open("dst.txt", O_RDONLY);
dst = mmap(NULL, len, PROT_READ, MAP_SHARED, dstfd, 0);
bzero(out, 64);
memcpy(out, dst, len);
printf("\nfile content:\n%s\n", out); munmap(dst, len);
close(dstfd);
return 0;
}

关于 mmap 函数的参数说明,这里就不细说了,大家可以参考 man-page。上面的代码运行结果如下:

关于内存映射就说到了,更深入的分析需要涉及到很多操作系统层面的东西。我对这些东西了解的也不多,所以就不继续分析了,惭愧惭愧。

2.5 其他操作

FileChannel 还有一些其他的方法,这里通过一个表格来列举这些方法,就不一一展开说明了。如下:

方法名 用途
position 返回或修改通道读写位置
size 获取通道所关联文件的大小
truncate 截断通道所关联的文件
force 强制将通道中的新数据刷新到文件中
close 关闭通道
lock 对通道文件进行加锁

以上所列举的方法用起来比较简单,大家自己写代码验证一下吧,这里就不贴代码了。

3.总结

以上章节对 NIO 文件通道的用法和部分方法的实现进行了简单分析。从上面的分析可以看出,NIO FileChannel 在实现上,实际上是对底层操作系统的一些 API 进行了再次封装,也就是一层皮。有了这层封装后,对上就屏蔽了底层 API 的细节,以降低使用难度。Java 为了提高开发效率,屏蔽了操作系统层面的细节。虽然 Java 可以屏蔽这些细节,但作为开发人员,我觉得我们不能也去屏蔽这些细节(虽然不了解这些细节也能写代码),有时间还是应该多了解了解这些底层的东西。毕竟要想往更高的层次发展,这些底层的知识必不可少。说到这里,感觉很惭愧,我的技术基础也很薄弱。大学期间没有意识到专业基础课的重要性,学了很多东西,但忽略了基础。好在工作不久后看了很多牛人的博客,也意识到了自己的不足。现在静下心来打基础,算是亡羊补牢吧。

好了,关于文件通道的内容这里就说到这,谢谢大家的阅读。

参考

本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处

作者:coolblog

本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

JAVA NIO之文件通道的更多相关文章

  1. java输入输出 -- java NIO之文件通道

    一.简介 通道是 Java NIO 的核心内容之一,在使用上,通道需和缓存类(ByteBuffer)配合完成读写等操作.与传统的流式 IO 中数据单向流动不同,通道中的数据可以双向流动.通道既可以读, ...

  2. Java IO和Java NIO在文件拷贝上的性能差异分析

    1.  在JAVA传统的IO系统中,读取磁盘文件数据的过程如下: 以FileInputStream类为例,该类有一个read(byte b[])方法,byte b[]是我们要存储读取到用户空间的缓冲区 ...

  3. Java NIO中的通道Channel(一)通道基础

    什么是通道Channel 这个说实话挺难定义的,有点抽象,不过我们可以根据它的用途来理解: 通道主要用于传输数据,从缓冲区的一侧传到另一侧的实体(如文件.套接字...),反之亦然: 通道是访问IO服务 ...

  4. Java NIO 读取文件、写入文件、读取写入混合

    前言 Java NIO(new/inputstream outputstream)使用通道.缓冲来操作流,所以要深刻理解这些概念,尤其是,缓冲中的数据结构(当前位置(position).限制(limi ...

  5. Java NIO中的通道Channel(二)分散/聚集 Scatter/Gather

    什么是Scatter/Gather scatter/gather指的在多个缓冲区上实现一个简单的I/O操作,比如从通道中读取数据到多个缓冲区,或从多个缓冲区中写入数据到通道: scatter(分散): ...

  6. java nio 映射文件内容到内存

    FileChannel 的一个最好的功能就是能将文件的某个部分直接映射到内存.这要感谢 FileChannel.map() 方法,这个方法有下面三个参数: mode:映射到内存需要指定下面三种模式之一 ...

  7. Java NIO.2 —— 文件或目录移动操作

    移动文件树是复制和删除的文件树的结合.实际上,有两种方式来完成文件的移动.一种是使用Files.move(), Files.copy(), 和Files.delete() 这三个方法:另一种是只使用F ...

  8. Java NIO.2 —— 文件或目录拷贝操作

    拷贝整个文件树是可以递归每个目录和文件调用 Files.copy()方法.在使用的时候有一下注意事项. 在往目录拷贝文件之前,首先要保证目录已经存在.拷贝源目录(不论是否为空)都会生成目标目录.整个任 ...

  9. Java NIO.2 —— 文件或目录删除操作

    文件删除 删除单个文件的操作很简单,如果要删除一个目录树的话,需要实现FileVisitor 接口然后递归地调用delete() 或deleteIfExists()方法.在看代码之前,需要注意一下问题 ...

随机推荐

  1. TCP协议的三次握手和四次分手

    HTTP连接 HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用. ...

  2. awk使用方法与案例介绍

    一.awk简介 awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大.简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切 ...

  3. 基于DirectShow的MPEG-4视频传输系统的研究与实现

    1 引言     近年来,随着国民经济的发展,社会各个部门对于视频监视系统的需求越来越多.但目前的很多监视系统都跟具体的硬件相关,必须要具体的采集卡的支持才能实现.所以有必要开发一种具有通用性的视频监 ...

  4. 笔记︱决策树族——梯度提升树(GBDT)

    每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- 本笔记来源于CDA DSC,L2-R语言课程所 ...

  5. 用vlc搭建简单流媒体服务器(UDP和TCP方式)

    .UDP(legacy)传统模式 .RTP方式 .RTSP方式 RTSP方式是通过RTP进行流媒体数据的传输的,VLC的实现也是基于UDP的.这种方式网上的参考资料比较多,我就不截图了,直接叙述命令行 ...

  6. visual studio 2010 Error: IntelliSense: identifier "DWORD" is undefined

    在自己工程里,添加别的工程文件时,出现改错误 解决方法 在文件前添加: using namespace std; 参考: http://www.programgo.com/article/502412 ...

  7. 手机浏览网页或打开App时莫名弹出支付宝领红包界面的原因及应对措施

    自从支付宝推出扫码领红包活动后,这种模式独特的赏金机制,短时间内吸引了大量的关注,但是随之也产生了很多的问题,比由于如在赏金的驱动下,微信群里铺天盖地的红包口令,朋友圈里各式各样的领红包二维码图片, ...

  8. 关于margin-top的一些特别问题

    当给子元素添加了margin-top的数值,浏览器解析的时候默认添加到父元素上解决的方法: 1 给父元素添加一个上边框border-top. 2 或者给子元素加个浮动. 3  给父元素添加overfl ...

  9. Qtree3题解(树链剖分(伪)+线段树+set)

    外话:最近洛谷加了好多好题啊...原题入口 这题好像是SPOJ的题,挺不错的.看没有题解还是来一篇... 题意: 很明显吧.. 题解: 我的做法十分的暴力:树链剖分(伪)+线段树+\(set\)... ...

  10. Luogu Dynamic Ranking (带修改的主席树)

    题目大意: 网址:https://www.luogu.org/problemnew/show/2617 给定一个序列a[1].a[2].....a[N],完成M个操作,操作有两种: [1]Q i j ...