Nodejs 中的 Stream

391 阅读5分钟

什么是 Stream

流的英文stream,流(Stream)是一个抽象的数据接口,Node.js中很多对象都实现了流,流是EventEmitter对象的一个实例,总之它是会冒数据(以 Buffer 为单位),或者能够吸收数据的东西,它的本质就是让数据流动起来

流的类型

Node.js,Stream 有四种流类型:

Stream的作用

我们先来看一个例子:

var http = require('http');
var fs = require('fs');

var server = http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});
server.listen(3000);

这段代码虽然可以正常执行,但存在一个显著的问题 —— 对于每一个客户端的请求,fs.readFile 接口都会把整个data.txt文件读入到内存中,然后再把结果返回给客户端。想想看,如果data.txt文件非常大,在响应大量用户的并发请求时,程序可能会消耗大量的内存,这样很可能会造成用户连接缓慢的问题。

其次,上面的代码可能会造成很不好的用户体验,因为用户在接收到任何的内容之前首先需要等待程序将文件内容完全读入到内存中。

然而这个问题,则正是 stream 发挥所长的地方:

var server2 = http.createServer(function (req, res) {
    var stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});
server2.listen(4000);

在上方代码段里,fs.createReadStream 创建了 data.txt 的可读流 *(Readable Stream)。对于创建的可读流,我们通过 .pipe() 接口来监听其 data 和 end 事件,并把 data.txt (的可读流)拆分成一小块一小块的数据(chunks),像流水一样源源不断地吐给客户端,而不再需要等待整个文件都加载到内存后才发送数据。

其中 .pipe 可以视为流的“管道/通道”方法,任何类型的流都会有这个 .pipe 方法去成对处理流的输入与输出。

为了方便理解,我们把上述两种方式 (不使用流/使用流) 处理为如下的情景 :

不使用流:

image.gif

使用流: image.gif

由此可以得知,使用流(stream)的形式,可以大大提升响应时间,又能有效减轻服务器内存的压力。

常用事件

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发,每当流将数据块的所有权移交给消费者时,则会触发 'data' 事件。
  • end - 没有更多的数据可读时触发,当流中没有更多数据可供消费时,则会触发 'end' 事件。
  • error - 在接收和写入过程中发生错误时, 则触发 'error' 事件。
  • finish - 所有数据已被写入到底层系统时,则触发 'finish' 事件。
  • close -当流及其任何底层资源(例如文件描述符)已关闭时,则会触发 'close' 事件。
  • pipe -当在可读流上调用 stream.pipe() 方法将此可写流添加到其目标集时,则触发 'pipe' 事件。。

Readable流

Readable流可以产出数据,你可以将这些数据传送到一个writable,transform或者duplex流中,只需要调用pipe()方法:

let { Readable } = require('stream');
let readable = Readable();
let source = ['a','b','c'];

readable.setEncoding('utf8');
readable._read = function () {
    let data = source.shift()||null;
    console.log('read:',data);
    this.push(data);
}
readable.on('end', function () {
  console.log('end')
})
readable.on('data', function (data) {
    console.log(data)
})
/*
输出:
read: a
read: b
a
read: c
b
read: null
c
end
*/

Readable通过实例_read或read方法,在需要数据时,_read()方法会自动调用。

  • _read方法结束的标志是 this.push(null)。
  • 当 readable 绑定 data 时 _read() 方法自动调用

data事件:当读入数据时触发data事件传入回调读到内容。
end事件:“消耗完”,需要满足两个条件:

  • 已经调用 push(null),声明不会再有任何新的数据产生
  • 缓存中的数据也被读取完

Writable流

可写流是对数据写入'目的地'的一种抽象。可写流的功能是作为下游,消耗上游提供的数据。

let { Writable } = require('stream');
var writable = Writable({
  write: function (data,_,next) {
      console.log(data);
      next && next();
  }
})

writable.write('a');
writable.write('b');
writable.write('c');
writable.end();

/* 输出:
<Buffer 61>
<Buffer 62>
<Buffer 63>
*/

write方法

  • write() 或 _write()的第三个参数 next 为回调函数,调用 next()表示写入完成,开始写下一个数据。
  • 必须调用 end() 方法来告诉 writable,所有数据均已写入。

管道流

管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。

如上面的图片所示,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的复制过程。

以下实例我们通过读取一个文件内容并将内容写入到另外一个文件中。

设置 input.txt 文件内容如下:

Nodejs Stream

创建 main.js 文件, 代码如下:

var fs = require("fs");

// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');

// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');

// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);

console.log("程序执行完毕");

链式流

链式是通过连接输出流到另外一个流并创建多个流操作链的机制。链式流一般用于管道操作。

接下来我们就是用管道和链式来压缩和解压文件。

创建 compress.js 文件, 代码如下:

var fs = require("fs");
var zlib = require('zlib');

// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('input.txt.gz'));
  
console.log("文件压缩完成。");