zoukankan      html  css  js  c++  java
  • 文件和Stream

    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

  • 相关阅读:
    移动函数的封装示例
    如何从不均衡类中进行机器学习
    DPM(Deformable Parts Model)--原理(一)
    K-means聚类算法
    机器学习中对核函数的理解
    总结:Bias(偏差),Error(误差),Variance(方差)及CV(交叉验证)
    技术干货
    神经网络入门
    目标函数、损失函数、代价函数
    地铁客流检测训练问题记录
  • 原文地址:https://www.cnblogs.com/raytheweak/p/10778978.html
Copyright © 2011-2022 走看看