I/O和文件

输入/输出(I/O)就是在内存和外部设备之间复制数据的过程。输入(input)就是从I/O设备复制数据到内存,输出(output)就是从内存复制数据到I/O设备。

一个文件可以理解成一串字节序列。所有的I/O设备,如网络、磁盘和终端,都被抽象为文件。所有的输入和输出都可以简化地抽象为对相应文件的读或写。对文件的操作包括:

  • 打开文件。一个应用程序可以请求内核”打开“一个文件(内核再请求硬件去访问某个I/O设备),内核会返回一个叫做描述符(descriptor)的非负整数,用来标识此文件。应用程序可以拿着这个描述符对此文件继续做其他操作。
  • 改变当前的文件位置。内核会记录每一个打开的文件的“文件位置” k,这个k指向字节序列中的某个字节,每次读写都会相应地改变k的值,初始为文件的开头(k=0)。应用程序也可以使用seek函数来显示设置当前文件位置。
  • 读写文件。读就是从文件中复制n个字节到内存,从当前的文件位置k开始,然后将k设为k + n。当读到文件的末尾时,会触发end-of-file (EOF)。类似地,写就是从内存中复制n个字节到文件,也是当前的文件位置k开始,然后将k设为k + n。
  • 关闭文件。当应用程序不再需要一个文件时,它会请求内核关闭这个文件,然后内核会释放所有与这个文件相关的数据结构对象(打开文件时创建的数据结构对象)。当一个应用程序终止时,内核会关闭所有它打开的文件。

目录和缓冲区

文件分为普通文件和目录( directory),以及其他类型,如socket。一个目录就是一个包含了一组链接(link)的文件,每个链接都是一个文件名,这个文件名指向一个文件(当然可能也是一个目录)。每个目录都包含...这两个链接,其中.指向这个目录本身,..指向这个目录的父目录。每个进程的上下文中都保存着一个叫做当前工作目录(current working directory)的状态。相对路径就是从当前工作目录开始,到某个文件的路径。

假设我们需要从一个ASCII文本文件中一行一行地读取,该如何做?一种办法是一次读取一个字节,然后判断一下这个字节是不是换行符。这种办法的缺点是效率不是很高,因为每次读取一个字节都需要切换到内核模式。一种更好的办法是先将一定数量的字节从文件中读到一个内部的缓冲区(buffer)中,然后再从这个缓冲区中一个字节一个字节地读取,当这个缓冲区被读完时,就自动再从文件中读一些字节过来。

Stream

高级语言中的I/O操作的API往往包含stream的概念。stream可以理解为对文件的抽象,但内部往往使用了缓冲区(作用同样是为了减少切换到内核模式的开销)。stream封装了操作系统提供的I/O模型,也就是我们不需要关心内核级别的函数,只需要对一个stream进行操作即可。类似文件,我们可以对一个Stream进行的操作包括读或写等等。

对于操作系统来说,EOF(end-of-file)是一个会被内核检测到的状态,对于磁盘文件,EOF会在当前文件位置超过此文件长度时发生,对于一次网络连接,EOF会在另一端关闭连接时发生。而Stream是对文件的高层抽象,也可以有自己的“EOF”(其实称作end of stream更合适)。以C#为例,StreamReader.ReadToEnd会一直读取Stream直到遇到EOF才返回,若stream是一个socket,则直到另一端的socket关闭了这次连接之前,StreamReader.ReadToEnd都不会返回,因为只有另一端关闭连接才会触发这一端的EOF。

var tcpClient = new TcpClient(host, port);
var connectionStream = tcpClient.GetStream();
using (StreamReader reader = new StreamReader(connectionStream))
{
await reader.ReadToEndAsync(); // 直到另一端关闭连接之前,会一直“阻塞”在这里
}

但是下面的代码,即使是有Connection: Keep-Alive的HTTP持久连接,也不会有问题:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
HttpWebResponse response = (HttpWebResponse)(await request.GetResponseAsync());
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
await reader.ReadToEndAsync(); // 即使另一边还没有关闭这次连接,也不会“阻塞”在这里
}

原因在于HttpWebResponse.GetResponseStream返回的并不是NetworkStream,而是一个特殊的Stream,叫作ConnectStream,专门用于读取一次HTTP响应的body。我们知道,HTTP是建立于TCP之上的,在一次TCP连接中,任何一端都可以对其socket无限制地读或写,理论上客户端可以无限地从socket中读取或等待读取数据,所以HTTP才会有Content-Length,来告诉客户端这一次响应有多大,该读取多少数据就够了。对于这个ConnectStream来说,当从底层的NetworkStream中读取的字节数达到指定的Content-Length大小时,就算是EOF。

所以,Stream相当于提供了一个抽象接口,不同的实现有不同的行为,但对外提供的功能是一致的。另外,Stream的读操作和操作系统提供的函数很类似,都会返回本次读取了多少字节,每次真正读取的字节数不一定等于输入参数中要求读取的字节数(特别是在网络编程时),有时候很容易忽视这一点。

参考

《深入理解计算机系统》第三版 第十章 System-Level I/O

文件和Stream的更多相关文章

  1. C# 流与文件(Stream & File & byte[])

    原文:https://www.cnblogs.com/long-gengyun/archive/2010/03/28/1698681.html 文件概述  文件在操作时表现为流,即流是从一些输入中读取 ...

  2. 通过文件流stream下载文件

    public ActionResult ShowLocalizedXML(int id) { string orderName = ""; string xmlString = G ...

  3. .net c# 提交包含文件file 的form表单 获得文件的Stream流

    1.前台html代码 要写一个有id的form,可是不能有runat="server"属性.由于一个页面中,有这个属性的form表单仅仅能有一个. 再要有一个有name的ifram ...

  4. C# 将文件转换为 Stream

    public Stream FileToStream(string fileName) { // 打开文件 FileStream fileStream = new FileStream(fileNam ...

  5. Java中的文件和stream流的操作代码

    1.Java中FileRead方法的运用代码及详解 package example2;import java.io.FileReader;import java.io.IOException;clas ...

  6. linux下载网页上的文件夹以及删除文件(stream)

    wget -nd -r -l1 --no-parent http://www.cs.virginia.edu/stream/FTP/Code/ 注:-nd 不创建目录:-r 递归下载:-l1只下载当前 ...

  7. 14、Java文件操作stream、File、IO

    1.文件操作涉及到的基本概念 File File类 是文件操作的主要对象中文意义就是 文件 顾名思意 万物皆文件,在计算上看到的所有东西都是文件保存,不管是你的图片.视频.数据库数据等等都是按照基本的 ...

  8. C#下载文件,Stream 和 byte[] 之间的转换

    stream byte 等各类转换 http://www.cnblogs.com/warioland/archive/2012/03/06/2381355.html using (System.Net ...

  9. C#:文件、byte[]、Stream相互转换

    一.byte[] 和 Stream /// <summary> /// byte[]转换成Stream /// </summary> /// <param name=&q ...

随机推荐

  1. MySQL性能优化(一):优化方式

    原文:MySQL性能优化(一):优化方式 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/v ...

  2. 设置队列中文件上的“X”号的点击事件+uploadLimit动态加1

    目的:1.设置文件队列中“x”号的点击事件 2.每次删除服务器文件后,把uploadLimit + 1: 'onUploadSuccess': function (file, data, respon ...

  3. java实现spark常用算子之distinct

    import org.apache.spark.SparkConf;import org.apache.spark.api.java.JavaRDD;import org.apache.spark.a ...

  4. react如何通过shouldComponentUpdate来减少重复渲染

    转自:https://segmentfault.com/a/1190000016494335 在react开发中,经常会遇到组件重复渲染的问题,父组件一个state的变化,就会导致以该组件的所有子组件 ...

  5. 多线程学习-- part 1 Thread

    一.Thread的使用 (1)sleep:进程等一会 (2)join:让并发处理变成串行 (3)start:启动线程的唯一方法,start()首先为线程分配必须的系统资源,调度线程运行并执行线程的ru ...

  6. 解决跨域问题,前端 live-server --port=1802 后端启动 localhost:1801,以及解决 vue 的 axios 请求整合

    测试的源码文件内容点击跳转 前端引入 vue.js 与 axios.min.js <script src="https://cdn.bootcss.com/vue/2.6.10/vue ...

  7. redis集群启动和关闭脚本

    创建startall.sh /usr/local/redis/bin/redis-server /usr/local/redis/redis-cluster/7001/redis.conf /usr/ ...

  8. 多个jar包合并成一个jar包(ant)

    https://blog.csdn.net/gzl003csdn/article/details/53539133 多个jar包合并成一个jar 使用Apache的Ant是一个基于Java的生成工具. ...

  9. java8学习之Collector同一性与结合性分析

    继续沿着上一次[http://www.cnblogs.com/webor2006/p/8311074.html]Collector接口的javadoc进行阅读,在继续阅读之前,其中有个比较难理解的地方 ...

  10. svn使用教程(收藏)

    SVN简介: 为什么要使用SVN? 程序员在编写程序的过程中,每个程序员都会生成很多不同的版本,这就需要程序员有效的管理代码,在需要的时候可以迅速,准确取出相应的版本. Subversion是什么? ...