Skip to content

Latest commit

 

History

History
566 lines (376 loc) · 28.9 KB

File metadata and controls

566 lines (376 loc) · 28.9 KB

七、流

Node 广泛使用流作为数据传输机制,例如,用于读写文件和通过网络套接字传输数据。第 5 章已经向你展示了标准流——stdinstdoutstderr。本章更详细地探讨了 Node 的 streams API,介绍了不同类型的流,它们是如何工作的,以及它们的各种应用。但是在开始之前,您应该知道,streams API 虽然是 Node 核心的重要部分,但在官方文档中被列为不稳定的。

什么是流?

流是一种在两点之间传输数据的机制。在行为方面,一个简单的花园软管提供了一个很好的类比。当你需要给你的草坪浇水时,你用一根软管把水源连接到洒水器上。当你打开水时,水通过软管流到喷水器。然后由洒水器来分配水。

流在概念上非常相似。例如,把给草坪浇水比作呼唤console.log()。在这种情况下,Node 应用充当水源。当调用console.log()时,水被打开,信息流经标准输出流。此时,Node 不再关心数据会发生什么。stdout流将数据传送到目的地。在这种情况下,目的地(喷洒器)几乎可以是任何东西——终端窗口、文件、另一个程序。

使用流

Node 支持几种类型的流,它们都继承自EventEmitter。每种类型的流行为略有不同。为了处理各种类型的流,首先导入stream核心模块(参见清单 7-1 )。

清单 7-1 。正在导入到stream模块

var Stream = require("stream");

导入stream模块会返回对Stream构造函数的引用。然后构造函数可以用来实例化新的流,如清单 7-2 所示。

清单 7-2 。使用stream模块创建新流

var Stream = require("stream");
var stream = new Stream();

可读流

可读流是数据的来源。一个典型的可读流是一个已经打开进行读取的文件。创建可读流的最简单方法是将流的readable属性分配给true,然后发出dataendcloseerror事件。以下部分探讨了如何使用这些事件。

data事件

您使用一个data事件来表示一个新的流数据片段(称为块)是可用的。对于发出的每个data事件,处理程序都被传递实际的数据块。许多应用将数据块作为二进制文件发出Buffer。这是官方文档规定的,尽管从技术上讲,任何数据都可以被发出。为了保持一致,建议data事件使用一个Buffer。清单 7-3 中的例子发出一个data事件,块被指定为Buffer

清单 7-3 。创建一个可读的流并发出一个data事件

var Stream = require("stream");
var stream = new Stream();

stream.readable = true;
stream.emit("data", new Buffer("foo"));

end事件

一旦一个流发送了它所有的数据,它应该发出一个单独的end事件。一旦发出了end事件,就不会再发出data事件。end事件不包括任何伴随数据。清单 7-4 中的例子创建了一个可读的流,它使用一个时间间隔在五秒钟内每秒发送一次数据。Date比较用来确定五秒钟过去了。此时,会发出一个end事件,间隔被清除。

清单 7-4 。一个可读的流,发出几个data事件,后跟一个end事件

var Stream = require("stream");
var stream = new Stream();
var duration = 5 * 1000; // 5 seconds
var end = Date.now() + duration;
var interval;

stream.readable = true;
interval = setInterval(function() {
  var now = Date.now();

  console.log("Emitting a data event");
  stream.emit("data", new Buffer("foo"));

  if (now >= end) {
    console.log("Emitting an end event");
    stream.emit("end");
    clearInterval(interval);
  }
}, 1000);

image 注意Date.now()方法返回当前日期和时间,指定为自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的毫秒数。

close事件

close事件用于指示流数据的底层源已经关闭。例如,当文件描述符关闭时,从文件中读取数据的流会发出一个close事件。并非所有可读的流都会发出一个close事件。因此,如果您实现自己的可读流,则不需要发出此事件。如果存在的话,close事件不包含额外的参数。清单 7-5 中的显示了一个close事件的例子。

清单 7-5 。发出一个close事件

var Stream = require("stream");
var stream = new Stream();

stream.readable = true;
stream.emit("close");

error事件

error事件用于指示数据流出现问题。例如,如果后备文件不存在,从文件中读取的流会发出一个error事件。向error事件处理程序传递一个Error对象,该对象详细解释了问题。清单 7-6 中的例子发出了一个error事件。

清单 7-6 。发出一个error事件

var Stream = require("stream");
var stream = new Stream();

stream.readable = true;
stream.emit("error", new Error("Something went wrong!"));

控制可读流

要暂停可读流,请使用pause()方法。当处于暂停状态时,可读流停止发出data事件(第 5 章stdin)的上下文中涉及pause())。清单 7-7 中显示了pause()的一个使用示例。

清单 7-7 。在stdin上调用pause()

process.stdin.pause();

默认情况下,stdin处于暂停状态(参见第 5 章)。为了从stdin或任何其他暂停的流中读取数据,首先使用resume()方法解除暂停。清单 7-8 中的例子显示了resume()的用法。调用resume()后,通过stdin到达的数据将导致data事件被发出。

清单 7-8 。在stdin上调用resume()

process.stdin.resume();

可写流

正如可读流是数据源一样,可写流是数据的目的地。要创建一个可写的流,将流的writable属性设置为true,并定义名为write()end()的方法。以下部分描述了这些方法,以及可写流的其他特性。

write()

方法负责将一大块数据写入数据流。数据块作为一个Buffer或字符串传递给write()。如果块是一个字符串,可选的第二个参数可用于指定编码。如果没有指定编码,默认情况下将使用 UTF-8。作为可选的最后一个参数,write()也接受一个回调函数。如果存在回调函数,则在成功写入数据块后调用该函数。

write()方法还返回一个布尔值,指示块是否被刷新到底层资源。如果返回true,则数据已经被刷新,流可以接受更多。如果返回了false,数据仍然在队列中等待写入。返回false还通知数据源停止发送数据,直到可写流发出一个drain事件。

清单 7-9 中的例子显示了对stdoutwrite()方法的调用。对write()的调用以字符串形式传递。因为文本是 UTF-8,所以省略了编码参数。回调函数因此成为第二个参数。

清单 7-9 。对stdoutwrite()方法的调用

var success = process.stdout.write("foo\n", function() {
  console.log("Data was successfully written!");
});
  console.log("success = " + success); 

在结果输出中(见清单 7-10 ,注意打印语句的执行顺序。对write()的调用完成,导致回调函数在事件循环中被调度。然而,执行从write()返回,然后继续,打印出success的值。此时,由于回调函数是事件循环中唯一剩下的项,因此它被执行,导致最终的打印语句运行。

清单 7-10 。运行清单 7-9 中代码的结果输出

$ node write.js
foo
success = true
Data was successfully written!

end()

用于表示数据流结束的end()方法可以在没有任何参数的情况下调用。但是,也可以使用与write()相同的参数调用它。对于只需要调用一次write(),然后再调用end()的情况,这是一个方便的快捷方式。

drain事件

write()返回false时,流的数据源应该不再发送数据。drain事件用于提醒源,处理完所有数据的可写流可以再次开始接收数据。drain事件不包括任何伴随数据。

finish事件

end()被调用并且不再有数据被写入时,流发出一个finish事件。它也没有提供额外的数据。与可能被多次发射的drain不同,finish可用于检测流的结束。

closeerror事件

像可读流一样,可写流也有行为方式相同的closeerror事件。

一个可写流的例子

现在让我们看一个非常简单的自定义可写流。当您希望在 Node 不支持的情况下使用流 API 时,自定义流非常有用。在清单 7-11 中的代码,改编自 James Halliday 的例子(https://github.com/substack/stream-handbook),流计算它处理的字节数。每次调用write()方法,总字节数都会增加缓冲区中的字节数。当调用end()时,它检查是否有缓冲区被传入。如果有,缓冲区被传递到write()。然后通过将writable属性设置为false并发出一个finish事件来关闭该流。最后,显示流处理的总字节数。

清单 7-11 。一个自定义的可写流,计算它处理的字节数

var Stream = require("stream");
var stream = new Stream();
var bytes = 0;

stream.writable = true;

stream.write = function(buffer) {
  bytes += buffer.length;
};

stream.end = function(buffer) {
  if (buffer) {
    stream.write(buffer);
  }

  stream.writable = false;
  stream.emit("finish");
  console.log(bytes + " bytes written");
};

管道

让我们回到花园软管的比喻。如果你的水管不够长,无法从水源到达你的草坪,那该怎么办?你可以用多根软管把它们连接起来。以类似的方式,数据流也可以链接在一起,以完成更大的任务。例如,假设我们有两个程序,程序 A 和程序 b。程序 A 的代码如清单 7-12 中的所示,它每秒钟生成一个随机的一位数整数(0–9)并将其输出到stdout。如清单 7-13 中的所示,程序 B 从stdin中读取任意数量的整数,并向stdout输出一个运行总和。现在你只需要一根软管来连接这两个程序。

清单 7-12 。一个随机的一位数整数生成器

setInterval(function() {
  var random = Math.floor(Math.random() * 10);

  console.log(random);
}, 1000);

清单 7-13 。对从stdin中读取的数字求和的应用

var sum = 0;

process.stdin.on("data", function(data) {
  var number = parseInt(data.toString(), 10);

  if (isFinite(number)) {
    sum += number;
  }

  console.log(sum);
});

process.stdin.resume();

image Math.random()返回一个介于 0(含)和 1(不含)之间的伪随机浮点数。将这个值乘以 10,如清单 7-12 所示,得到一个介于 0(含)和 10(不含)之间的随机浮点数。Math.floor()返回小于传入参数的最大整数。因此,清单 7-12 生成一个介于 0(含)和 9(含)之间的随机整数。

这些隐喻的软管被称为管道。如果您做过任何 shell 编程,您无疑会遇到管道。它们允许一个流程的输出流直接进入另一个流程的输入流。在 shell 编程中,管道操作符|实现管道。清单 7-14 显示了如何使用管道从命令行连接两个示例程序。在示例中,程序 A 的输出通过管道传输到程序 B 的输入。当您运行该命令时,您将看到一串数字,代表程序 B 中的sum变量的值,以每秒一个的速度打印到控制台。

清单 7-14 。从一个程序到另一个程序的管道输出

$ node Program-A.js | node Program-B.js

pipe()

在 Node 应用中,可以使用pipe()方法将流连接在一起,该方法有两个参数:一个作为数据目的地的必需的可写流和一个用于传入选项的可选对象。在清单 7-15 中的简单例子中,从stdinstdout创建了一个管道。当这个程序运行时,它监听用户的输入。当按下Enter键时,用户输入的任何数据都会回显到stdout

清单 7-15 。使用pipe()方法将stdin连接到stdout

process.stdin.pipe(process.stdout);

pipe()可选的第二个参数是一个可以保存单个布尔属性的对象,end。如果endtrue(默认行为),当源流发出其end事件时,目标流关闭。然而,如果end被设置为false,则目标流保持打开,因此可以将额外的数据写入目标流,而无需重新打开它。

image 注意当与文件或终端窗口相关联时,标准流的行为是同步的。例如,对stdout的写操作会阻塞程序的其余部分。然而,当它们通过管道传输时,它们的行为是异步的,就像任何其他流一样。此外,可写的标准流stdoutstderr不能被关闭,直到进程终止,不管end选项的值是多少。

回到可写流的例子

清单 7-11 引入一个定制的可写流时,你看不到它做任何事情。既然您已经了解了管道,那么可以向这个示例流提供一些数据。清单 7-16 展示了这是如何做到的。最后三行特别值得注意。首先,创建一个具有相同源和目的地的管道。接下来,流发出一个data事件,随后是一个end事件。

清单 7-16 。从清单 7-11 中的向自定义可写流传输数据

var Stream = require("stream");
var stream = new Stream();
var bytes = 0;

stream.writable = true;

stream.write = function(buffer) {
  bytes += buffer.length;
};

stream.end = function(buffer) {
  if (buffer) {
    stream.write(buffer);
  }

  stream.writable = false;
  stream.emit("finish");
  console.log(bytes + " bytes written");
};

stream.pipe(stream);
stream.emit("data", new Buffer("foo"));
stream.emit("end");

这些事件触发可写流的write()end()方法。结果输出如清单 7-17 所示。

清单 7-17 。运行清单 7-16 中的代码得到的输出

$ node custom-stream.js
3 bytes written

文件流

在第 6 章的中,你看到了如何使用fs模块的readFile()writeFile()方法,以及它们的同步对应物来读写文件。这些方法非常方便,但是有可能导致应用中的内存问题。作为复习,以清单 7-18 中的readFile()为例,其中一个名为foo.txt的文件被异步读取。一旦读取完成,回调函数被调用,文件的内容被打印到控制台。

清单 7-18 。使用fs.readFile() 读取文件

var fs = require("fs");

fs.readFile(__dirname + "/foo.txt", function(error, data) {
  console.log(data);
});

为了理解这个问题,假设您的应用是一个每秒接收成百上千个连接的 web 服务器。还假设所有被服务的文件,不管什么原因,都非常大,并且在将数据返回给客户机之前,每次请求都使用readFile()将文件从磁盘读入内存。当调用readFile()时,它在调用其回调函数之前缓冲文件的全部内容。由于繁忙的服务器正在同时缓冲许多大文件,内存消耗可能会激增。

那么,如何避免所有这些肮脏的事情呢?事实证明,文件系统模块提供了以流的形式读写文件的方法。然而,这些方法createReadStream()createWriteStream()与大多数其他的fs方法不同,它们没有同步等价物。因此,第 6 章有意跳过了它们,直到读者对 streams 有了更彻底的介绍。

createReadStream()

顾名思义,createReadStream()用于将文件作为可读流打开。最简单的形式是,createReadStream()接受一个文件名作为参数,并返回一个类型为ReadStream的可读流。因为在fs模块中定义的ReadStream类型继承自标准可读流,所以它可以以同样的方式使用。

清单 7-19 中的例子显示createReadStream()正在读取一个文件的内容。data事件处理程序用于在数据通过流时打印出数据块。由于一个文件可以包含多个块,process.stdout.write()用于显示这些块。如果使用了console.log(),并且文件比一个块大,那么输出将包含原始文件中没有的额外的换行符。当end事件被接收时,console.log() 被用来简单地打印一个尾随的新行到输出。

清单 7-19 。使用fs.createReadStream()读取文件

var fs = require("fs");
var stream;

stream = fs.createReadStream(__dirname + "/foo.txt");

stream.on("data", function(data) {
  var chunk = data.toString();

  process.stdout.write(chunk);
});

stream.on("end", function() {
  console.log();
});()

ReadStreamopen事件

如前所述,ReadStream类型继承自基本可读流。这意味着ReadStream可以增强基本流的行为。T2 事件是一个很好的例子。当传递给createReadStream()的文件名被成功打开时,流发出一个open事件。用单个参数调用open事件的处理函数,该参数是流使用的文件描述符。通过获得文件描述符的句柄,createReadStream()可以与其他文件系统方法结合使用,这些文件系统方法使用诸如fstat()read()write()close()这样的文件描述符。在清单 7-20 的例子中,当调用open事件处理程序时,文件描述符被传递给fstat()以显示文件的统计数据。

清单 7-20 。使用来自open事件处理程序的文件描述符调用fstat()

var fs = require("fs");
var stream;

stream = fs.createReadStream(__dirname + "/foo.txt");

stream.on("open", function(fd) {
  fs.fstat(fd, function(error, stats) {
    if (error) {
      console.error("fstat error:  " + error.message);
    } else {
      console.log(stats);
    }
  });
});

options论证

createReadStream()接受的可选的第二个参数被命名为options。如果存在,这个参数是一个对象,它的属性允许你修改createReadStream()的行为。options参数支持的各种属性在表 7-1 中描述。

表 7-1。选项参数支持的属性描述

|

属性名称

|

描述

| | --- | --- | | fd | 现有的文件描述符。这默认为null。如果提供了一个值,就没有必要指定一个文件名作为createReadStream()的第一个参数。 | | encoding | 指定流的字符编码。默认为null。表 5-1 描述了支持的编码类型。 | | autoClose | 如果为true,当发出errorend事件时,文件自动关闭。如果false,文件不关闭。默认为true。 | | flags | flags参数传递给open()。可用值列表见表 6-3。默认为"r"。 | | mode | mode参数传递给了open()。默认为"0666"。 | | start | 文件中开始读取的字节索引。默认值为零(文件的开头)。 | | end | 文件中要停止读取的字节索引。只有在同时指定了start的情况下才能使用。默认为Infinity(文件的结尾)。 |

清单 7-21 的例子中,利用了createReadStream()options参数,由open()返回的文件描述符被传递给createReadStream()。因为使用了一个现有的文件描述符,所以将null而不是文件名作为第一个参数传递给createReadStream()。该示例还使用了startend选项来跳过文件的第一个和最后一个字节。fstat()方法用于确定文件大小,以便适当设置end。该示例还包括许多错误检查。例如,如果使用目录而不是普通文件,代码将无法正常工作。

清单 7-21 。利用createReadStream()options自变量

var fs = require("fs");

fs.open(__dirname + "/foo.txt", "r", function(error, fd) {
  if (error) {
    return console.error("open error:  " + error.message);
  }

  fs.fstat(fd, function(error, stats) {
    var stream;
    var size;

    if (error) {
      return console.error("fstat error:  " + error.message);
    } else if (!stats.isFile()) {
      return console.error("files only please");
    } else if ((size = stats.size) < 3) {
      return console.error("file must be at least three bytes long");
    }

    stream = fs.createReadStream(null, {
      fd: fd,
      start: 1,
      end: size - 2
    });

    stream.on("data", function(data) {
      var chunk = data.toString();

      process.stdout.write(chunk);
    });

    stream.on("end", function() {
      console.log();
    });
  });
});

createWriteStream()

要创建与文件相关联的可写流,请使用createWriteStream()。与createReadStream()非常相似,createWriteStream()将一个文件路径作为其第一个参数,将一个可选的options对象作为其第二个参数,并返回一个WriteStream的实例,这是在fs模块中定义的一种数据类型,从基本的可写流类型继承而来。

清单 7-22 中的例子展示了数据如何通过管道传输到用createWriteStream()创建的可写文件流。在本例中,创建了一个可读的文件流,它从foo.txt中提取数据。然后,数据通过可写流传输到一个名为bar.txt的文件中。

清单 7-22 。将可读文件流管道传输到可写文件流

var fs = require("fs");
var readStream = fs.createReadStream(__dirname + "/foo.txt");
var writeStream = fs.createWriteStream(__dirname + "/bar.txt");

readStream.pipe(writeStream);

createWriteStream()options参数与createReadStream()使用的略有不同。表 7-2 描述了传递给createWriteStream()options对象可以包含的各种属性。

表 7-2 。createWriteStream()的 options 参数支持的属性

|

属性名称

|

描述

| | --- | --- | | fd | 现有的文件描述符。这默认为null。如果提供了一个值,就没有必要指定一个文件名作为createWriteStream()的第一个参数。 | | flags | flags参数传递给open()。可用值列表见表 6-3。默认为"w"。 | | encoding | 指定流的字符编码。默认为null。 | | mode | mode参数传递给了open()。默认为"0666"。 | | start | 文件中开始写入的字节索引。默认值为零(文件的开头)。 |

WriteStreamopen事件

WriteStream类型也实现了它自己的open事件,当目标文件被成功打开时,该事件被发出。open事件的处理程序接受文件描述符作为唯一的参数。清单 7-23 中显示了一个可写文件流的示例open事件处理程序。这个例子只是打印出代表打开文件的文件描述符的整数。

清单 7-23 。可写文件流的open事件处理程序

var fs = require("fs");
var stream = fs.createWriteStream(__dirname + "/foo.txt");

stream.on("open", function(fd) {
  console.log("File descriptor:  " + fd);
});

bytesWritten属性

WriteStream类型跟踪写入底层流的字节数。这个计数可以通过流的bytesWritten属性获得。清单 7-24 显示了如何使用bytesWritten。回到清单 7-22 中的例子,一个文件的内容使用一个可读的流读取,然后使用一个可写的流传输到另一个文件。然而,清单 7-24 包含了一个可写流的finish事件的处理程序。当发出finish事件时,这个处理程序被调用,并显示已经写入文件的字节数。

清单 7-24 。使用WriteStreambytesWritten属性

var fs = require("fs");
var readStream = fs.createReadStream(__dirname + "/foo.txt");
var writeStream = fs.createWriteStream(__dirname + "/bar.txt");

readStream.pipe(writeStream);

writeStream.on("finish", function() {
  console.log(writeStream.bytesWritten);
});

使用 zlib模块压缩

压缩是使用比原始表示更少的比特对信息进行编码的过程。压缩很有用,因为它允许使用更少的字节来存储或传输数据。当需要检索数据时,只需将其解压缩到原始状态。压缩广泛用于 web 服务器,通过减少网络上发送的字节数来缩短响应时间。但是,应该注意,压缩不是免费的,并且会增加响应时间。在归档数据时,压缩也通常用于减小文件大小。

Node 的核心zlib模块提供了使用流实现的压缩和解压缩 API。因为zlib模块是基于流的,所以它允许使用管道轻松压缩和解压缩数据。具体来说,zlib提供了使用 Gzip、Deflate 和通缩箭头进行压缩的绑定,以及使用 Gunzip、Inflate 和 Inflate 箭头进行解压缩的绑定。由于所有这三种方案都提供了相同的接口,因此在它们之间切换只是改变方法名的问题。

清单 7-25 中的例子使用 Gzip 压缩一个文件,从导入fszlib模块开始。接下来,zlib.creatGzip()方法用于创建 Gzip 压缩流。数据源input.txt用于创建可读的文件流。同样,创建一个可写文件流,将压缩数据输出到input.txt.gz。清单的最后一行通过读取未压缩的数据并将其通过 Gzip 压缩器来执行实际的压缩。然后,压缩数据通过管道传输到输出文件。

清单 7-25 。使用 Gzip 压缩来压缩文件

var fs = require("fs");
var zlib = require("zlib");
var gzip = zlib.createGzip();
var input = fs.createReadStream("input.txt");
var output = fs.createWriteStream("input.txt.gz");

input.pipe(gzip).pipe(output);

要测试压缩应用,只需创建input.txt,并在其中存储 100 个A字符(文件大小应为 100 字节)。接下来,运行 Gzip 压缩器。文件input.txt.gz应该以 24 字节的文件大小创建。当然,压缩文件的大小取决于几个因素。第一个因素是未压缩数据的大小。然而,压缩的有效性还取决于原始数据中重复模式的数量。我们的示例实现了出色的压缩,因为文件中的所有字符都是相同的。通过用一个B替换一个A,压缩文件的大小从 24 字节跳到 28 字节,即使源数据的大小相同。

压缩后的数据可能更小,但不是特别有用。为了处理压缩的数据,我们需要对其进行解压缩。清单 7-26 中的显示了一个 Gzip 解压缩应用的例子。zlib.createGunzip()方法创建一个执行解压缩的流。来自清单 7-25input.txt.gz文件被用作可读流,它通过管道传输到 Gunzip 流。解压缩后的数据通过管道传输到一个新的输出文件output.txt

清单 7-26 。使用 Gunzip 解压缩 Gzip 压缩文件

var fs = require("fs");
var zlib = require("zlib");
var gunzip = zlib.createGunzip();
var input = fs.createReadStream("input.txt.gz");
var output = fs.createWriteStream("output.txt");

input.pipe(gunzip).pipe(output);

放气/充气和放气箭头/充气箭头

Deflate 压缩方案可以用作 Gzip 的替代方案。DeflateRaw 方案类似于 Deflate,但是省略了 Deflate 中的 zlib 头。如前所述,这些方案的用法与 Gzip 相同。用于创建 Deflate 和 DeflateRaw 流的方法是zlib.createDeflate()zlib.createDeflateRaw()。类似地,zlib.createInflate()zlib.createInflateRaw()用于创建相应的解压缩流。一个额外的方法,zlib.createUnzip(),以同样的方式使用,它可以通过自动检测压缩方案来解压缩 Gzip 和 Deflate 压缩数据。

便利方法

前面提到的所有流类型都有相应的一步压缩/解压缩字符串或Buffer的便利方法。这些方法是gzip()gunzip()deflate()inflate()deflateRaw()inflateRaw()unzip()。它们都将一个Buffer或字符串作为第一个参数,将一个回调函数作为第二个参数。回调函数将错误条件作为第一个参数,将压缩/解压缩的结果(作为Buffer)作为第二个参数。清单 7-27 显示了如何使用deflate()unzip()来压缩和解压缩一个字符串。压缩和解压缩后,数据被打印到控制台。如果一切正常,存储在data变量中的相同字符串会显示出来。

清单 7-27 。使用方便的方法进行压缩和解压缩

var zlib = require("zlib");
var data = "This is some data to compress!";

zlib.deflate(data, function(error, compressed) {
  if (error) {
    return console.error("Could not compress data!");
  }

  zlib.unzip(compressed, function(error, decompressed) {
    if (error) {
      return console.error("Could not decompress data!");
    }

    console.log(decompressed.toString());
  });
});

摘要

本章介绍了数据流的概念。您已经看到了如何创建自己的流,以及如何使用现有的流 API,比如文件流。接下来的章节将展示网络编程环境中的流。您还将学习如何生成和控制子进程,这些子进程公开它们自己的标准流。