假设我们现在要盖一座房子,我们买了一些砖块,厂家正在送货。现在我们有两个选择,一是等所有砖块都到了以后再开始动工;二是到一批砖块就开始动工,砖块到多少我们就用多少。

这两种方式哪种效率更高呢?显然是第二种。这就是流(stream)的理念。在计算机科学中,流是随时间可用的一系列数据元素。就像传送带运输物品一样,使用流可以实现一次处理一个数据元素。

在 NodeJS 中,streamopen in new window 模块实现了流的功能。即使我们没有直接使用过这个模块,我们也间接使用过流,比如读写文件、网络等。

水流,信息流

信息就像水流一样,以比特流(strem of bits)的形式从一个地方流到另一个地方。比如读取文件,信息就从磁盘流向了应用程序。

但是,流的两端处理信息的速度是不同的,通常流的一端会比另一端要慢,因此就需要一个缓存来作为缓冲(buffer)。

如下图所示,上面的水龙头水流较大,下面的水龙头水流较小,因此需要一个容器来暂时存储下面的水龙头来不及处理的水。

流与缓存

NodeJS 中流的基本原理也是这样的,stream 模块实现了这些能力。

在 NodeJS 中有两种基本的流可以使用:

  • 可读流(Readable Streams)
  • 可写流(Writable Streams)

同时还有两种读写混合的流:

  • 双工流(Duplex Streams)-- 可读可写的流
  • 转换流(Transform Streams)-- 可以转换数据的双工流

可读流(Readable Stream)

可读流可以从一个地方读取数据,比如从文件中读取信息。读取的数据可以暂时存放在可读流中的缓存(Buffer)open in new window里,防止应用程序无法及时处理。

可读流

常见的可读流有 process.stdinopen in new windowfs.createReadStreamopen in new window 以及 HTTP 服务中的 IncomingMessageopen in new window 对象。

可写流(Writable Stream)

可写流可以将数据写到一个地方,比如将数据写入文件中。为了防止因为写入目标处理速度太慢导致数据丢失,写入的数据可以暂存在可写流内部的缓存(Buffer)中。

可写流

常用的可写流有 process.stdoutopen in new windowprocess.stderropen in new windowfs.createWriteStreamopen in new window.

双工流(Duplex Streams)

双工流是可读流和可写流的混合体。连接到双工流之后,应用程序既可以从流中读取数据,也可以向流中写入数据。在双工流中,可读流和可写流有各自独立的缓存(Buffer)。

双工流

最常用的双工流就是 net.Socketopen in new window

转换流(Transform Stream)

转换流是更加特殊的混合流,在转换流中,可读流是通过某种方式连接到可写流上的。

转换流

最常见的转换流是有 Cipheropen in new window 创建的流。在这个流中,应用程序写入数据,然后再从流中读取加密后的数据。

管道(Pipe)

通常流在连接到一起之后才能发挥更大的作用。我们通过管道来连接流。

比如我们可以将一个可读流连接到一个可写流或者双工流上,仅仅使用可读流的 pipe() 方法即可。

常见的管道场景就是复制文件。将 fs.createReadStream() 创建的流通过 pipe() 方法连接到 fs.createWriteStream() 创建的流上去。

使用流复制数据

我们可以将流连接到多个其他流上。这在一些需要重复读取原始数据的场景中非常有用。因为可读流只能读取一次数据,因此我们可以通过 pipe() 方法将可读流连接到多个流上,这样这些被连接的流就可以直接消费数据,不需要创建多个可读流。

const fs = require('fs')

const original = fs.createReadStream('./original.txt')
const copy1 = fs.createWriteStream('./copy1.txt')
const copy2 = fs.createWriteStream('./copy2.txt')

original.pipe(copy1)
original.pipe(copy2)
1
2
3
4
5
6
7
8

使用流复制数据

高水位线控制(highWaterMark)

在最开始的例子中,我们通过水箱蓄水的例子描述了流的缓存特性。因为上方的水流始终比下方的水流快,水箱中的水越来越多,终究会超过水箱的容积而溢出。

因此我们需要一个高水位警戒线,当水箱中的水位高于这个警戒线的时候,就需要通知上方的水龙头暂时停止放水了。

高水位线控制

在流中也是同样的原理,可读流和可写流内部都有缓存,这些缓存的最大可存储量是系统的可用内存量。NodeJS 流通过 highWaterMark 这个配置项来控制缓存中的水位线。

举个例子,如下图,可读流连接到可写流之后,可写流通过 highWaterMark 来检测缓存中的水位是否过高,高于这条线之后,可写流会通知可读流暂停写入数据。

高水位线控制

需要注意的是,highWaterMark 只是一个警示线,并不是一个硬性约束条件。也就是说,如果自定义的流没有正确处理这个警示线的话,可能会导致数据丢失。

流的应用

我们来举个例子综合说明如何使用流。

假设我们有一个裁减图片的应用程序。用户将图片的地址告诉应用程序,应用程序从网络上读取原始图片,裁减之后再返回给用户。那么我们可以借助于流来实现这个应用程序的功能,如下图。

裁减图片的应用

关注微信公众号,获取最新推送~