资讯专栏INFORMATION COLUMN

流(stream)是怎么一回事

MangoGoing / 1546人阅读

摘要:文件流,系统标准输入输出流,标准错误流,还有一开始提到的流,还有一些后台技术如对请求响应流的抽象,都可以见到流的概念。语言的库中定义了打开文件流时必须指定的集中打开方式,表示用于读取,用于写入,用于读写。

—— 对这个问题的思考来源于前几天对 Java Socket 编程的尝试,TCP 协议要求建立一个 Socket 连接(著名的三次握手)之后才能进行通信,而连接双方进行数据的发送与接受,都是通过对输入输出的机制来完成的。

流的概念

流作为概念应该是语言无关的。文件IO流,Unix系统标准输入输出流,标准错误流(stdin, stdout, stderr),还有一开始提到的 TCP 流,还有一些 Web 后台技术(如Nodejs)对HTTP请求/响应流的抽象,都可以见到流的概念。

K&R 在 C Programming Language 书中提到流是这样定义的:

流 (stream) 是与磁盘或其它外围设备关联的数据的源或目的地。

可以把流理解成是对程序外界交换数据的一种抽象,这里的外界限定是有必要的,通常不会把程序内部的数据流动抽象为流,毕竟在程序内部,数据流动是由函数调用、返回来完成的。而当我们使用三个标准IO流时,我们关心的是怎样通过它们与外界交互;当我们使用文件流时,我们关心的是将内存中的数据持久化到磁盘文件中(或从磁盘中读数据导内存)。

于是数据从 A 处“流”向 B 处,可以类比像水流一样从高处流向低处。在水流动的过程中,作为最基本物理组成单位的水分子是不变的,相应的数据流也有它最小的组成单位。在不同的编程语言中,这个最小单位通常是字节流(二进制流)中的字节,或者字符流(文本流)中的字符。

——但不会是其他数据类型,就像我们从来没听说过数字流?,或者浮点数流,甚至数组流?

因为字节是计算机保存数据的最终形式,而字符是其它数据结构序列化后的表现形式,也是人可以阅读的形式。与外界的交互需要这些通用的格式。不关心数据的内容,只需要完整地传输原始数据时,考虑字节流即可;关心传输字符和字符串时,就需要对字符流进行操作,stdio.h头文件里那一大坨输入输出函数就是干这个的。比如fgetc(FILE *stream)从文本流中读入一个字符。

另一方面,根据数据流动的方向,可以再抽象出输出流和输入流的概念。从程序内部到外部的流向是输出流,从程序外部到内部的流向是输入流。

C 语言的stdio.h库中定义了打开文件流时必须指定的集中打开方式,"r"表示用于读取,"w"用于写入,"r+"用于读写。类似地,Java 语言的java.io包中包含了InputStream, OutputStream 明确区分的输入流类和输出流类,并且二者都是抽象类,意味着必须根据需要使用它们各自的子类进行实例化。

通过流操作实现(最)简单的文件拷贝

根据实际的代码可以帮助理解stream,下面是一段用C语言标准库实现的最简单的文件拷贝功能。

出于学习目的,这段代码偷懒没有任何容错功能,是典型的反面教材, 不过 whatever 了,不信你真拿去编译一下,是真的可以完整拷贝文件!除了不能拷贝目录,不能拷贝不存在的文件,不能拷贝文件权限,不能漏掉目的文件名或者路径,不能灵活处理文件软链接硬链接。等等等等blahblah(所以其实连看上去很简单的cp程序也是要有一大坨因素要考虑和支持的(啊跑题了

// mini_cp.c
#include 
#define BUFFER_SIZE 512

int main(int argc, char *argv[])
{ 
  // 从命令行参数中获得 SOURCE 和 DES 文件流
  FILE *src = fopen(argv[1], "rb");
  FILE *des = fopen(argv[2], "wb");
  
  long int num;
  
  // buffer 是读写的缓冲数组
  char buffer[BUFFER_SIZE];

  while(!feof(src)) {
    num = fread(buffer, sizeof(char), BUFFER_SIZE, src);
    fwrite(buffer, sizeof(char), num, des);
  }

  fclose(src);
  fclose(des);

  return 0;
}

这个自制的mini_cp程序不难理解,核心的逻辑可以分解为三个步骤:

打开源文件流FILE *src和目的文件流FILE *des

循环执行 { 每次从src流读取最多512字节的数据 => 并写入des流 } 直到源文件读取结束

关闭文件流

核心逻辑是非常清晰明了的,这样的逻辑也是流操作的普遍原理,尝试其他语言的实现,其实都已经大同小异,往往都少不了一个缓冲区的概念(或对象)。

来看一下 Java 版本的同等实现:

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Copy {
    
    private static final int BUFFER_SIZE = 512;

    public static void main(String[] args) throws IOException {
        
        File srcFile = new File(args[0]);
        File desFile = new File(args[1]);

        int recvBytesSize;
        byte[] buffer = new byte[BUFFER_SIZE];

        FileInputStream in = new FileInputStream(srcFile);
        FileOutputStream out = new FileOutputStream(desFile);

        while((recvBytesSize = in.read(buffer)) != -1) {
            out.write(buffer, 0, recvBytesSize);
        }

        in.close();
        out.close();

    }
}

面向对象味更浓(代码更冗长)了有木有?但也正是因为面向对象,Java 把理论上的 stream 抽象为类,让我们直接获得类的实例(即对象),从而对对象进行操作。还是挺不赖的是吧,虽然代码更长了没错,但是更 OO 啊~

写到这里已经能回答流基本是怎么一回事了,那么最后顺便再来放一段拷贝程序的ruby实现;

require "fileutils"
FileUtils.cp("SOURCE.txt", "DEST.txt")

哈?

嗯。

... That"s why we love Ruby...(逃。。。

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

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

相关文章

  • 《Java8实战》-第六章读书笔记(用收集数据-01)

    摘要:收集器用作高级归约刚刚的结论又引出了优秀的函数式设计的另一个好处更易复合和重用。更具体地说,对流调用方法将对流中的元素触发一个归约操作由来参数化。另一个常见的返回单个值的归约操作是对流中对象的一个数值字段求和。 用流收集数据 我们在前一章中学到,流可以用类似于数据库的操作帮助你处理集合。你可以把Java 8的流看作花哨又懒惰的数据集迭代器。它们支持两种类型的操作:中间操作(如 filt...

    EscapedDog 评论0 收藏0
  • Flink 灵魂两百问,这谁顶得住?

    摘要:由于配置流是从关系型数据库中读取,速度较慢,导致实时数据流流入数据的时候,配置信息还未发送,这样会导致有些实时数据读取不到配置信息。从数据库中解析出来,再去统计近两周占比。 Flink 学习 https://github.com/zhisheng17/flink-learning 麻烦路过的各位亲给这个项目点个 star,太不易了,写了这么多,算是对我坚持下来的一种鼓励吧! showI...

    Guakin_Huang 评论0 收藏0
  • Java8学习小记

    摘要:但有一个限制它们不能修改定义的方法的局部变量的内容。如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。 2014年,Oracle发布了Java8新版本。对于Java来说,这显然是一个具有里程碑意义的版本。尤其是那函数式编程的功能,避开了Java那烦琐的语法所带来的麻烦。 这可以算是一篇Java8的学习笔记。将Java8一些常见的一些特性作了一个概要的...

    CHENGKANG 评论0 收藏0

发表评论

0条评论

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