资讯专栏INFORMATION COLUMN

白话Java I/O模型

VincentFF / 2455人阅读

摘要:因为所有的数据从最底层讲是字节,那么就可以使用字节流这个概念去指代数据动态转移这个过程。而数据的转移,就是把一堆字节流从运往。创建内存中的中转区域,然后将上面的文件的字节流直接接入到这个。然后再从把字节流输出到对应的。

I/O的很多操作和使用,其实并不是一个非常直观的概念,特别是打开文件、创建buffer。这对于终端用户来讲是个非常奇葩和奇怪的过程。我只是想要从一个文件里读取内容,从过程上来讲,我只需要知道:

读取的source文件

写入的目的地

那我干嘛要去关心神马打开文件、创建stream和buffer?!

所以,要理解I/O这一套东西以及它所涉及的stream、buffer,你必须先理解计算机的底层是如何工作的。如果没有这一步的底层基础理论做支撑,所有的I/O操作将无法变得直观。

为理解I/O所需要用到的底层知识并不算多,就几点:

计算机的对数据的操作一定要经过内存。无论你是计算出来的数据、从硬盘中的文件读取的数据、从网络中读取到的数据,都必须要经过内存。source data先到内存,然后内存再到destination

既然涉及到内存,就存在一个“有限”的问题。所有的程序都是往内存跑,无疑这一个非常宝贵的资源。那么你的“传输数据”任务,并没有那么高的优先级可以任意地去占有这个资源。你有且只能获取一部分,特别地,还应该是相对较小的一部分区域,来供你做数据传输。

所有在计算机中的数据,无论是文字、图片、声音都是0、1这样的bits。所以,最为通用的传输数据方式必然是面向字节的,也就是围绕字节这个概念来做的。

面对以上这些现实,你不得不在programming时考虑上述问题。因为你不是终端用户只需要一个简单的接口。你是细节的操作者,必须对以上限制做出具体的可操作性的回应。

因为你只能使用一小部分的内存空间做数据转移,所以这就必然需要一个buffer的概念去指代内存这部分的小空间。所以你要创建buffer并为它分配大小,然后所有的数据转移都通过这块小小的中转站。(这就像是一个城市的快递中心,所有全国各地发往这个地区的快件,都必须通过这个中转站来做统一调配。)这是对计算机的体系架构——所有数据都必须通过内存——所作出的回应。

因为所有的数据从最底层讲是字节(bit),那么就可以使用字节流这个概念去指代数据动态转移这个过程。而数据的转移,就是把一堆字节流从source运往destination。但由于上面的原因,这个过程无法直接完成,所以你必须把字节流从:source --> buffer --> destination,或者destination --> buffer --> source

由于传输的都是字节流,所以你需要一个工具把这个stream给开垦出来,所以你需要有一个File式的对象,从上面可以取得一个Channel或者Stream,也即是把file转换为字节流的池子,以便直接把文件的字节流拿给buffer。它们就像是data的矿源,通过buffer这辆采矿的小车,不断地把矿石(data)从矿源(source)运到外面(destination)。

有了这部分的知识,我们再来看“Java中的NIO是如何读取文件的”就不会变得怪异了。

RandomAccessFile aFile = new RandomAccessFile("test.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

让我们将之前讨论的过程复现一遍:

首先创建RandomAccessFile对象,来提供真实文件test.txt在Java中对应的对象(可以理解为一个bean)。这个对象将提供各种服务来配合Java内部各种机制的操作。无疑,提供一个Channel是它的本质工作之一。

创建内存中的中转区域buffer,然后将上面的文件Channel的字节流直接接入到这个buffer。

然后再从buffer把字节流输出到System.out.print对应的std io

接下来可以深入更多的细节。

由于buffer是被重复利用的部分,所以这涉及到清理buffer的概念buf.clear()。那你可能会说,为什么不可以自动地清理buffer?因为这里的buffer是一个底层的基础服务。对于上层的应用来讲,有些场景是需要清理buffer,有些场景是需要复用buffer的内容。你怎么可以一概而论地认为所有应用场景都是只需要消费一次buffer中的数据呢?所以,作为底层设施来讲,你必须提供足够的灵活性,让developer自己决定是否需要清理buffer。

再来比较奇怪的是flip()这部分,为什么需要对buffer做flip操作呢?这就涉及到内存中buffer的管理问题。

这里的管理方式其实很常见,在内存中,基本上都是以数组的形式提供堆栈结构来管理数据。那么,这就涉及到对这个数组的操作问题。你要有一个表征position的指针来指导数据的写入方向。

对“写数据”这个过程来讲,数据的position指针是从起始点,index为0的点,逐步增大index来写数据的。只要一直在做“写”操作,这个指针就会在buffer中不断地往index增大的方向移动。

显然,当你需要读数据时,你是需要从这个buffer的起始点再开始。所以,你需要一个操作把position指针复位到起始位置,然后从这个地方开始不断地往下读。(一个值得思考的问题是:为什么不引入两个position指针,一个用来读,一个用来写。这样不是就不用把“读”的position指针复位了吗?)

如果多走一步,为了验证flip()是否真的在让position指针复原,你还可以使用以下代码:

System.out.println("Read " + bytesRead);

// switch the buffer from writing mode into reading mode
buf.flip();

while (buf.hasRemaining()) {
    System.out.println((char) buf.get());
}

System.out.println("---------------------------");

// reset the pointer back to original point again
buf.flip();

while (buf.hasRemaining()) {
    System.out.println((char) buf.get());
}

可以看到,从buffer中又一次获取到了同样的信息。

从这个例子可以看到很多深层次的东西。例如,为什么你必须了解底层的内存运作机制、操作系统的运转机制?因为这些细节决定了你该以什么样的方式去设计你的编程模式,也决定了你应该如何去理解编程语言中提供的一些机制,或者为什么一个库应该会这样设计。这是一切具体行动的现实。

“具备什么功能”是这底层基础设施提供的封装好的API。但你要做的是programming的工作,不得不利用底层的基础设施去构建新的服务和产品。如果没办法理解这些底层机制,你就没办法真正地去构建东西。很多的问题,其实可以被绕过又或是不可能被实现,不在于逻辑有问题,而是单纯的信息差,你并不知道这个构建出的抽闲概念下面隐藏的真实东西。

如果能够理解内存的利用方式,那么,“分片、以stack的形式来做操作”的模式将成为你本能的一部分。进而,涉及到的position移动或者flip()的指针回调问题,就会成为你的直观。

当然,积累这部分的基础知识是非常枯燥和乏味的。但如同所有的基本功,它们不会在短期内为你提供足够的回报,但却会为你将来形成正确的“直觉”和“直观”做出巨大的贡献。

任何的抽象概念都具备直观,只不过这个直观所依赖的基础不同。抽象如“概率论基础”,其“形象”的直观,其实是数学系本科所学的“经典概率论”,否则你会迷失在“测度论”的细节里。但这个“直观”对于其它专业的人来讲,并不直观,甚至是异常复杂。而这个就是所谓的牢固的前提基础知识。进一步,如果你想要学好“概率论基础”这样高度抽象的topic,你必须先夯实“经典概率论”这个基础,必须先建立对它的深刻认知。否则,你对“概率论基础”的理解根本无从谈起。

同样的,如果你希望理解类似于编程语言中I/O库的设计、理解各类缓存中间件、消息队列中间件的设计,你必须要先建立“计算机如何运作”这个前提基础。只有你熟悉了计算机的运作方式,能够以计算机底层的习惯去思考问题、处理问题,你才能够看到各种组件设计的直观,才会看到各种莫名其妙的“绕路”到底是在解决什么、是为了什么。

所以,这样一个学习过程是任何抽象技能所避免不了的。你必须通过反复的阅读和练习来掌握第一层的基础概念,熟悉到让这一层的抽象变成你脑海中的一个条件反射式的直观。再这个新建立的直观本能基础上,你才可以去理解更高层次的抽象。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72616.html

相关文章

  • JAVA NIO 一步步构建I/O多路复用的请求模型

    摘要:为解决这问题,我们发现元凶处在一线程一请求上,如果一个线程能同时处理多个请求,那么在高并发下性能上会大大改善。这样一个线程可以同时发起多个调用,并且不需要同步等待数据就绪。表示当前就绪的事件类型。 JAVA NIO 一步步构建I/O多路复用的请求模型 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 文章一:JAVA ...

    X_AirDu 评论0 收藏0
  • NIO网络相关基础知识

    摘要:操作系统是能够获取到事件操作完成的事件,基于回调函数机制和操作系统的操作控制实现事件检测机制。 前面的文章NIO基础知识介绍了Java NIO的一些基本的类及功能说明,Java NIO是用来替换java 传统IO的,NIO的一些新的特性在网络交互方面会更加的明显。 Java 传统IO的弊端     基于JVM来实现每个通道的轮询检查通道状态的方法是可行的,但仍然是有问题的,检查每个通道...

    1fe1se 评论0 收藏0
  • 白话原型和原型链

    摘要:二原型和原型链其实,原型这个名字本身就很容易产生误解,原型在百度词条中的释义是指原来的类型或模型。对象的原型是通过创建的对象,其原型是而的原型是,从而两个原型对象和有了继承关系三和这是容易混淆的两个属性。 关于原型和原型链的介绍,网上数不胜数,但能讲清楚这两个概念的很少,大多数都是介绍各种对象、属性之间如何指来指去,最后的结果就是箭头满天飞,大脑一团糟。本文将从这两个概念的命名入手,用...

    lijy91 评论0 收藏0
  • I/O模型Java NIO源码分析

    摘要:最近在学习网络编程和相关的知识,了解到是模式的网络框架,但是提供了不同的来支持不同模式的网络通信处理,包括同步异步阻塞和非阻塞。因为的版本使用的的模式,而则希望使用模式,而且版本没有将的部分配置项暴露出来,比如说和。   最近在学习Java网络编程和Netty相关的知识,了解到Netty是NIO模式的网络框架,但是提供了不同的Channel来支持不同模式的网络通信处理,包括同步、异步、...

    yuanxin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<