你能解释一下流的概念吗?

我理解流是字节序列的表示。每个流都提供了将字节读写到其给定的后备存储的方法。但溪流的意义何在?为什么后台存储本身不是我们交互的对象?

不知什么原因,我就是不喜欢这个概念。我读了很多文章,但我觉得我需要一个类比。

26777 次浏览

流的目的是在您和后台存储之间提供一个抽象层。因此,使用流的给定代码块不需要关心后台存储是磁盘文件、内存等等…

这只是一个概念,另一个层次的抽象,让你的生活更容易。它们都有共同的接口,这意味着你可以以类似管道的方式组合它们。例如,编码到base64,然后压缩,然后将其写入磁盘,所有这些都在一行中!

关键是你不应该知道后台存储是什么——它只是一个抽象。实际上,可能甚至没有备份存储——你可能从网络读取数据,数据根本就没有被“存储”。

如果你写的代码能够在文件系统、内存、网络或任何支持流思想的东西上工作,那么你的代码就会更加灵活。

此外,流通常是链接在一起的——你可以有一个流来压缩放入其中的任何内容,将压缩的表单写入另一个流,或者加密数据,等等。在另一端是反向链,解密,解压缩等等。

流是字节序列的抽象。其思想是,您不需要知道字节来自何处,只需以标准化的方式读取它们。

例如,如果你通过流处理数据,那么数据来自文件、网络连接、字符串、数据库中的blob等等,对你的代码来说都无关紧要。

与备份存储本身交互本身并没有什么问题,除了它将您绑定到备份存储实现。

把流看作是抽象的数据源(字节、字符等)。它们抽象了具体数据源的实际读写机制,可以是网络套接字、磁盘上的文件或来自web服务器的响应。

流是一种抽象,它提供了一组用于与数据交互的标准方法和属性。通过从实际的存储介质中抽象出来,可以编写代码而不完全依赖于该介质是什么,甚至不依赖于该介质的实现。

一个很好的类比可能是考虑一个袋子。你不在乎一个包是什么做的,也不在乎当你把东西放进去的时候它能做什么,只要这个包能发挥它的功能,你就能把东西拿出来。流为存储介质定义了袋的概念,就像袋的概念为袋的不同实例(如垃圾袋、手提包、背包等)定义的那样——交互规则。

我认为您需要考虑后备存储本身通常只是另一种抽象。内存流很容易理解,但是文件完全不同,这取决于您使用的文件系统,而不考虑您使用的是什么硬盘驱动器。事实上,并不是所有的流都位于备份存储的顶部:网络流基本上就是流。

流的意义在于我们将注意力限制在重要的事情上。通过标准抽象,我们可以执行公共操作。例如,即使您今天不想在文件或HTTP响应中搜索url,也不意味着您明天就不想这样做了。

流最初是在内存与存储相比很小的时候设想的。仅仅读取一个C文件就可能是一个很大的负载。最小化内存占用是非常重要的。因此,很少需要加载的抽象是非常有用的。今天,它在执行网络通信时同样有用,而且在处理文件时也很少有限制。以一般方式透明地添加诸如缓冲之类的东西的能力使它更加有用。

为了增加回声室,流是一个抽象,所以您不关心底层存储。当您考虑有和没有流的场景时,这是最有意义的。

文件在很大程度上是无趣的,因为除了我熟悉的非基于流的方法之外,流并没有做太多事情。让我们从网络文件开始。

如果我想从互联网上下载一个文件,我必须打开一个TCP套接字,建立一个连接,并接收字节,直到没有更多的字节。我必须管理一个缓冲区,知道预期文件的大小,并编写代码来检测连接何时断开并适当地处理这个问题。

假设我有某种TcpDataStream对象。我用适当的连接信息创建它,然后从流中读取字节,直到它说没有任何字节。流处理缓冲区管理、数据结束条件和连接管理。

通过这种方式,流使I/O更容易。当然,您可以编写一个TcpFileDownloader类来完成流所做的工作,但是这样您就有了一个特定于TCP的类。大多数流接口只提供Read()和Write()方法,任何更复杂的概念都由内部实现处理。因此,您可以使用相同的基本代码来读写内存、磁盘文件、套接字和许多其他数据存储。

之所以选择“流”这个词,是因为它(在现实生活中)与我们使用它时想要传达的意思非常相似。

让我们暂时忘记后备存储,开始思考与水流的类比。你会收到持续不断的数据流,就像河水不断地流动一样。你不一定知道数据从何而来,大多数情况下你也不需要知道;无论是来自文件、套接字还是任何其他源,这并不(不应该)真正重要。这非常类似于接收一股水流,你不需要知道它从哪里来;无论是来自湖泊、喷泉还是其他来源,这并不(不应该)真的重要。

也就是说,一旦您开始认为您只关心获得所需的数据,而不管数据来自何处,其他人谈论的抽象概念就会变得更加清晰。您开始认为可以包装流,并且您的方法仍然可以完美地工作。例如,你可以这样做:

int ReadInt(StreamReader reader) { return Int32.Parse(reader.ReadLine()); }


// in another method:
Stream fileStream = new FileStream("My Data.dat");
Stream zipStream = new ZipDecompressorStream(fileStream);
Stream decryptedStream = new DecryptionStream(zipStream);
StreamReader reader = new StreamReader(decryptedStream);


int x = ReadInt(reader);

如您所见,在不改变处理逻辑的情况下更改输入源变得非常容易。例如,要从网络套接字而不是文件读取数据:

Stream stream = new NetworkStream(mySocket);
StreamReader reader = new StreamReader(stream);
int x = ReadInt(reader);

尽可能的简单。而且美妙之处还在继续,因为您可以使用任何类型的输入源,只要您可以为它构建一个流“包装器”。你甚至可以这样做:

public class RandomNumbersStreamReader : StreamReader {
private Random random = new Random();


public String ReadLine() { return random.Next().ToString(); }
}


// and to call it:
int x = ReadInt(new RandomNumbersStreamReader());

看到了吗?只要您的方法不关心输入源是什么,您就可以以各种方式自定义源。抽象允许您以一种非常优雅的方式将输入与处理逻辑解耦。

请注意,我们自己创建的流没有备份存储,但它仍然完美地满足了我们的目的。

所以,总的来说,流只是一个输入源,隐藏(抽象)了另一个源。只要你不打破抽象,你的代码就会非常灵活。

我使用的可视化是传送带,不是在真实的工厂里,因为我对此一无所知,而是在卡通工厂里,物品沿着线移动,被盖章、装箱、计数和检查,由一系列愚蠢的设备完成。

你有做一件事的简单组件,例如一个把樱桃放在蛋糕上的设备。这个设备有一个无樱桃蛋糕的输入流,和一个有樱桃蛋糕的输出流。用这种方式组织处理有三个优点值得一提。

首先,它简化了组件本身:如果你想把巧克力糖衣放在蛋糕上,你不需要一个复杂的设备,知道蛋糕的一切,你可以创造一个愚蠢的设备,把巧克力糖衣粘在任何东西上(在漫画中,这甚至不知道下一个东西不是蛋糕,而是怀尔E.大狼)。

其次,你可以通过将这些设备按不同的顺序排列来创造不同的产品:也许你想让你的蛋糕在樱桃上放糖衣,而不是樱桃在糖衣上,你可以简单地通过在生产线上交换设备来做到这一点。

第三,设备不需要管理库存、装箱或开箱。最有效的聚合和包装方式是多变的:也许今天你把你的蛋糕装成48个一盒,然后一卡车一卡车地送出去,但明天你想要送出6个一盒的定制订单。这种变化可以通过在生产线的开始和结束处更换或重新配置机器来适应;生产线中间的樱桃机器不需要改变一次处理不同数量的项目,它总是一次处理一个项目,它不需要知道它的输入或输出是如何分组的。

我所见过的对流的最好解释是SICP第三章。(你可能需要阅读前两章才能理解,但无论如何你都应该这样做。: -)

它们对字节根本不使用sterams,而是整数。我从中得到的要点是:

  • 流是延迟列表
  • 在某些情况下,急于提前计算所有内容的计算开销是惊人的
  • 我们可以用流来表示无限长的序列

流已经是一个比喻,一个类比,所以真的没有必要再提供另一个。你可以把它想象成一个管道,里面有水流,水实际上是数据,管道是流。我认为这是一种双向管道如果流是双向的。它基本上是一种常见的抽象,用于在一个或两个方向上有数据流或数据序列的事物。

在语言中,如c#, VB。Net, c++, Java等等,流的比喻被用于很多事情。有文件流,在文件流中,你打开一个文件,可以连续地从流中读取或写入;有一些网络流,其中对流的读写是从底层已建立的网络连接中读取和写入的。仅用于写入的流通常被称为输出流,如的例子;类似地,仅用于读取的流被称为输入流,如的例子。

流可以执行数据转换或编码(例如,.Net中的SslStream将耗尽SSL协商数据并将其隐藏起来;TelnetStream可能对您隐藏Telnet协商,但提供对数据的访问;Java中的ZipOutputStream允许您写入zip归档中的文件,而不必担心zip文件格式的内部问题。

您可能会发现的另一个常见的东西是允许您编写字符串而不是字节的文本流,或者一些语言提供了允许您编写基本类型的二进制流。您将在文本流中发现一个常见的东西是字符编码,您应该知道这一点。

一些流也支持随机访问,如 example。另一方面,由于显而易见的原因,网络流不会。

类似UNIX的操作系统也支持带有程序输入和输出的流模型,如在这里所述。

流表示可以按顺序访问的对象序列(通常是字节,但不一定是这样)。流的典型操作:

  • 读入一个字节。下次读取时,您将获得下一个字节,以此类推。
  • 从流中读入几个字节到数组中
  • 查找(移动流中的当前位置,以便下次读取时从新位置获取字节)
  • 写一个字节
  • 将数组中的几个字节写入流
  • 从流中跳过字节(这类似于读取,但忽略数据。或者如果你喜欢,它就像seek,但只能向前。)
  • 将字节推回输入流(这类似于read的“undo”—您将几个字节推回流,以便下次读取时您将看到。它偶尔对解析器有用,如下所示:
  • Peek(查看字节而不读取它们,以便它们仍然在流中待读取)

一个特定的流可能支持读(在这种情况下,它是一个“输入流”),写(“输出流”)或两者都支持。并不是所有的溪流都是可搜索的。

回推是相当罕见的,但是您总是可以将它添加到流中,方法是将真正的输入流包装在另一个包含内部缓冲区的输入流中。读取来自缓冲区,如果您将数据回推,则数据将被放置在缓冲区中。如果缓冲区中没有任何内容,则推回流从实际流中读取。这是一个简单的“流适配器”的例子:它位于输入流的“末端”,它本身就是一个输入流,它做一些原始流没有做的额外的事情。

Stream是一个有用的抽象,因为它可以描述文件(实际上是数组,因此seek很简单),也可以描述终端输入/输出(除非缓冲,否则是不可查找的)、套接字、串行端口等。因此,你可以编写这样的代码:“我想要一些数据,我不在乎它从哪里来,也不在乎它是如何到这里的”,或者“我将生成一些数据,它会发生什么完全取决于我的调用者”。前者接受输入流参数,后者接受输出流参数。

我能想到的最好的比喻是,溪流是一条传送带,向你走来或离开你(有时两者兼而有之)。你从输入流中取出东西,你把东西放到输出流中。有些传送带你可以认为是从墙上的一个洞里出来的——它们是不可寻找的,阅读或写作是一次性的交易。一些传送带就摆在你面前,你可以在溪流中选择你想读/写的位置——这就是寻找。

但是,正如IRBMe所说,最好从它所提供的操作来考虑流(这些操作因实现而异,但有很多共同点),而不是通过物理类比。流是“你可以读或写的东西”。当您开始连接流适配器时,您可以将它们想象成一个带传输器的盒子,将其连接到其他流,然后盒子对数据执行一些转换(压缩数据,或将UNIX换行器更改为DOS换行器,等等)。管道是对这个比喻的另一个彻底测试:在这里您创建了一对流,这样您在其中一个流中写入的任何内容都可以从另一个流中读取。想想虫洞吧:-)

除了上面提到的东西,还有一种不同类型的流——在函数式编程语言(如Scheme或Haskell)中定义的流——一种可能无限的数据结构,由一些函数按需生成。

我长话短说,我刚才漏掉了这个词:

流是队列通常存储在包含任何类型数据的缓冲区中。

(现在,既然我们都知道队列是什么,就没有必要进一步解释了。)

之所以选择“流”这个词,是因为它(在现实生活中)与我们使用它时想要传达的意思非常相似。

开始思考与水流的类比。你会收到持续不断的数据流,就像河水不断地流动一样。你不一定知道数据从何而来,大多数情况下你也不需要知道;无论是来自文件、套接字还是任何其他源,这并不(不应该)真正重要。这非常类似于接收一股水流,你不需要知道它从哪里来;无论是来自湖泊、喷泉还是其他来源,这并不(不应该)真的重要。

当我第一次听说流媒体时,它是在带有网络摄像头的在线直播上下文中。所以,一个主机播放视频内容,另一个主机接收视频内容。这是流媒体吗?嗯…是的……但直播是一个具体的概念,我认为这个问题指的是流媒体这个抽象的概念。看到https://en.wikipedia.org/wiki/Live_streaming

让我们继续。


视频并不是唯一可以流媒体的资源。音频也可以流式传输。我们现在谈论的是流媒体。参见https://en.wikipedia.org/wiki/Streaming_media。音频可以通过多种方式从源传输到目标。因此,让我们比较一些数据传递方法。

经典文件下载 传统的文件下载并不是实时的。在使用该文件之前,您必须等待下载完成。< / p >

< >强渐进式下载 渐进式下载块将数据从流媒体文件下载到临时缓冲区。该缓冲区中的数据是可行的:缓冲区中的音频-视频数据是可播放的。因为用户可以在下载的同时观看/收听流媒体文件。快进和倒带是可能的,当然是在缓冲区内。不管怎样,渐进式下载并不是直播。< / p >

< >强流 实时发生,大量数据。流媒体在直播中实现。正在收听广播的客户端不能快进或倒带。在视频流中,数据在回放后被丢弃。< / p >

流服务器与客户端保持双向连接,而Web服务器在服务器响应后关闭连接。


音频和视频并不是唯一可以流媒体的东西。让我们看看PHP手册中的流的概念。

流是显示可流行为的资源对象。那 是,它可以是读取写入的线性方式,并且可以是 能够fseek()到流中的任意位置。 链接:https://www.php.net/manual/en/intro.stream.php < / p >

在PHP中,资源是对外部源(如文件、数据库连接)的引用。换句话说,流是一个可以读取或写入的源。因此,如果你使用了fopen(),那么你已经使用了流。

一个文本文件被流式处理的例子:

// Let's say that cheese.txt is a file that contains this content:
// I like cheese, a lot! My favorite cheese brand is Leerdammer.
$fp = fopen('cheese.txt', 'r');


$str8 = fread($fp, 8); // read first 8 characters from stream.


fseek($fp, 21); // set position indicator from stream at the 21th position (0 = first position)
$str30 = fread($fp, 30); // read 30 characters from stream


echo $str8; // Output: I like c
echo $str30; // Output: My favorite cheese brand is L

Zip文件也可以流式传输。最重要的是,流媒体并不局限于文件。HTTP, FTP, SSH连接和输入/输出也可以流式传输。


维基百科对流媒体的概念是怎么说的?

在计算机科学中,流是数据元素的序列 随时间推移可用。流可以看作是传送带上的物品 皮带一次加工一个,而不是大批量加工。

参见:https://en.wikipedia.org/wiki/Stream_%28computing%29

维基百科链接到这个:https://srfi.schemers.org/srfi-41/srfi-41.html 关于流,作者是这样说的:

流,有时称为惰性列表,是一种顺序数据结构 只包含按需计算的元素。流要么为空 或者是cdr中有一个流的pair。因为流的元素是 只有在访问时才计算,流可以是无限的

流实际上是一种数据结构。


我的结论是:流是一种包含数据的源,可以按顺序读取或写入数据。流不会一次读取源包含的所有内容,而是按顺序读取/写入。


有用的链接:

  1. http://www.slideshare.net/auroraeosrose/writing-and-using-php-streams-and-sockets-zendcon-2011提供了一个非常清晰的表示
  2. https://www.sk89q.com/2010/04/introduction-to-php-streams/
  3. http://www.netlingo.com/word/stream-or-streaming.php
  4. http://www.brainbell.com/tutorials/php/Using_PHP_Streams.htm
  5. http://www.sitepoint.com/php-streaming-output-buffering-explained/
  6. http://php.net/manual/en/wrappers.php
  7. http://www.digidata-lb.com/streaming/Streaming_Proposal.pdf
  8. http://www.webopedia.com/TERM/S/streaming.html
  9. https://en.wikipedia.org/wiki/Stream_%28computing%29
  10. https://srfi.schemers.org/srfi-41/srfi-41.html

另一点(对于读取文件的情况):

  1. stream可以允许你在finished reading all content of the file之前做其他事情。
  2. 可以节省内存,因为不需要一次加载所有文件内容。

到目前为止给出的答案都很好。我只是提供另一个来强调流不是字节序列或特定于编程语言,因为这个概念是通用的(尽管它的实现可能是唯一的)。我经常在网上看到大量关于SQL、C或Java的解释,这些解释在文件流处理内存位置和低级操作时是有意义的。但是,他们经常讨论如何创建文件流,并在给定的语言中对潜在的文件进行操作,而不是讨论流的概念。

这个比喻

如上所述,stream是一个隐喻,是更复杂的东西的抽象。为了激发你的想象力,我提供了一些其他的比喻:

  1. 你想把一个空池子装满水。实现这一目的的一种方法是将软管连接到水龙头上,将软管的一端放在水池中,然后打开水。

软管就是溪流

  1. 类似地,如果你想给你的车加油,你会走到加油站,把喷嘴插入油箱,然后通过挤压锁杆打开阀门。

软管,喷嘴和相关的机构,让气体流入你的油箱是流

  1. 如果你需要去上班,你会开始从家开车走高速公路到办公室。

高速公路就是溪流

  1. 如果你想和某人交谈,你会用耳朵听,用嘴巴说。

你的耳朵和眼睛是溪流

希望你在这些例子中注意到,流的隐喻只存在于允许某些东西通过它(或者在高速公路的情况下在它上面),而并不总是表示它们正在传输的东西。这是一个重要的区别。我们不认为耳朵是一连串的单词。如果没有水流经,软管仍然是软管,但我们必须将其连接到水龙头上,才能正确地工作。汽车并不是唯一一种可以穿越高速公路的交通工具。

因此,只要流是连接文件,就可以存在没有数据通过的流。

去除抽象

接下来,我们需要回答几个问题。我要用文件来描述流什么是文件?我们如何读取文件?我将尝试在保持一定抽象级别以避免不必要的复杂性的同时回答这个问题,并将使用相对于linux操作系统的文件概念,因为它的简单性和可访问性。

什么是文件?

文件是抽象的:)

或者,我可以简单地解释,一个文件是描述文件的一部分数据结构和实际内容的一部分数据。

数据结构部分(在UNIX/linux系统中称为inode)标识关于内容的重要信息,但不包括内容本身(或文件的名称)。它保留的信息之一是内容开始位置的内存地址。因此,有了文件名(或linux中的硬链接)、文件描述符(操作系统关心的数字文件名)和内存中的起始位置,我们就有了可以称为文件的东西。

(关键是“文件”是由操作系统定义的,因为最终必须处理它的是操作系统。是的,文件更复杂)。

到目前为止一切顺利。但我们怎么拿到文件的内容,比如给你男友的情书,这样我们就能打印出来了?

读取文件

如果我们从结果开始并向后移动,当我们在计算机上打开一个文件时,它的全部内容都会显示在屏幕上供我们阅读。但如何?答案是非常有条理。文件本身的内容是另一种数据结构。假设有一个字符数组。我们也可以把它看成一个字符串。

那么我们如何“读取”这个字符串呢?通过找到它在内存中的位置并遍历我们的字符数组,一次一个字符,直到到达文件字符的末尾。换句话说就是一个程序。

流是在程序被调用时被“创建”的,它的内存位置是连接到连接到。就像我们的水管的例子一样,如果软管没有连接到水龙头上,它是无效的。在流的情况下,它必须连接到文件才能存在。

流可以进一步细化,例如,接收输入的流或将文件内容发送到标准输出的流。UNIX/linux立即为我们连接并保持打开3个文件流,stdin(标准输入),stdout(标准输出)和stderr(标准错误)。流本身可以构建为数据结构或对象,这允许我们通过它们执行更复杂的数据流操作,如打开流、关闭流或错误检查流连接的文件。c++的cin就是一个流对象的例子。

当然,如果您愿意,您可以编写自己的流。

定义

流是一段可重用的代码,它抽象了处理数据的复杂性,同时提供了对数据执行的有用操作。

流是一个高度抽象的隐喻,严格的合同. xml是一个抽象的概念。这意味着你可以按顺序不用担心差距操作对象。也就是说,流必须没有真空或间隙。其中的物体连续地按顺序排列。这样,我们就不用担心在处理流的过程中突然遇到真空,也不用担心在产生流的过程中故意留下真空。换句话说,我们不需要考虑在处理或产生流时出现空白的情况。我们不可能偶遇它,也不可能故意制造它。如果正在构造流,则不能在流中留下任何间隙。

换句话说,如果有一个缺口,它一定不是一个流。当你把一个序列称为一个流时,要么你被保证在它里面没有间隙,要么你必须保证在你产生的序列里没有间隙。

回顾一下,想象一条水流。它最大的特点是什么?

连续!

溪流的抽象精神就是关于它的。