Node.js Stream流

本文内容大部分从 Node’s Streams 文章中翻译得来。

Node Streams因难以使用甚至难以理解而享有盛誉。好了,我对你有个好消息-情况不再如此。

多年来,开发人员在那里创建了许多程序包,其唯一目的是使使用流变得更加容易。但是,在本文中,我将重点介绍本机Node Streams API。

 

Stream到底是什么?

流是数据的集合–就像数组或字符串一样。不同之处在于,流可能不会一次全部可用,并且不必容纳在内存中。当处理大量数据或一次来自一个外部chunk的大量数据时,这使流真正变得强大。

但是,流不仅涉及处理大数据。它们还使我们的代码具有可组合性。就像我们可以通过传递其他较小的Linux命令来组成功能强大的Linux命令一样,我们可以在Node中使用流进行完全相同的操作。

picturenode4

const grep = ... // A stream for the grep output
const wc = ...   // A stream for the wc input

grep.pipe(wc)

 Node中的许多内置模块都实现了流接口:

Readable StreamsWritable Streams
HTTP response, on the clientHTTP requests, on the client
HTTP requests, on the serverHTTP responses, on the server
fs read streamsfs write streams
zlib streamszlib streams
crypto streamscrypto streams
TCP socketsTCP sockets
child process stdout & stderrchild process stdin
process.stdinprocess.stdout, process.stderr

 上面的列表中有一些本机Node对象的示例,它们也是可读或可写的流。其中一些对象既是可读写流,例如TCP套接字,zlib和crypto流。

请注意,对象也密切相关。虽然HTTP响应是客户端上的可读流,但它是服务器上的可写流。这是因为在HTTP情况下,我们基本上从一个对象(http.IncomingMessage)读取并写入另一个对象(http.ServerResponse)。

另请注意,在子进程中,stdio流(stdin,stdout,stderr)如何具有逆流类型。这提供了一种使用主流程stdio流向这些子流程stdio流进出管道的真正简便方法。

 

一个Stream实例

理论很棒,但通常不是100%令人信服。让我们看一个示例,说明在内存消耗方面流可以在代码中产生的差异。

首先创建一个大文件:

const fs = require("fs");
const file = fs.createWriteStream("./big.file");

for (let i = 0; i <= 1e6; i++) {
  file.write(
    "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"
  );
}

file.end();

看看我用来创建那个大文件的东西。可写流!

fs模块可用于使用流接口读取和写入文件。在上面的示例中,我们正在通过可写流向该big.file循环写入100万行。

运行上面的脚本会生成一个大约400 MB的文件。

这是一个专门用于服务big.file的简单Node网络服务器:

const fs = require("fs");
const server = require("http").createServer();

server.on("request", (req, res) => {
  fs.readFile("./big.file", (err, data) => {
    if (err) throw err;

    res.end(data);
  });
});

server.listen(8000);

服务器收到请求后,它将使用异步方法fs.readFile提供大文件。但是,这并不是说我们阻止了事件循环或其他任何活动。一切都很好,对吧?对?

好吧,让我们看看在运行服务器,连接到服务器并监视内存时会发生什么。

运行服务器时,它以正常的内存量8.7 MB

picturenode5

然后我连接到服务器。注意消耗的内存发生了什么:

picturenode6

内存消耗跃升至434.8 MB

我们基本上将整个big.file内容放入内存中,然后再将其写到响应对象中。这是非常低效的。

HTTP响应对象(上面代码中的结果)也是可写流。这意味着,如果我们有一个表示big.file内容的可读流,我们可以将这两个管道相互传递,并获得几乎相同的结果,而无需占用约~400 MB的内存。

Node的fs模块可以使用 createReadStream 方法为我们提供任何文件的可读流。我们可以通过管道将其传递给响应对象:

const fs = require("fs");
const server = require("http").createServer();

server.on("request", (req, res) => {
  const src = fs.createReadStream("./big.file");
  src.pipe(res);
});

server.listen(8000);

现在,当您连接到该服务器时,发生了一件神奇的事情(请查看内存消耗):

picturenode7

发生了什么?

当客户要求该大文件时,我们一次流化一个大块,这意味着我们根本不将其缓冲在内存中。内存使用量增加了约25 MB,仅此而已。
您可以将此示例推到极限。用500万行(而不是100万行)重新生成big.file,这将使文件的大小超过2 GB,实际上大于Node中的默认缓冲区限制。
如果您尝试使用fs.readFile提供该文件,则默认情况下根本无法(可以更改限制)。但是使用fs.createReadStream,将2 GB的数据流传输到请求者完全没有问题,而且最重要的是,进程内存使用情况大致相同。

 

Stream 对象的原型链

它的对象层级为:

自身属性(由fs.ReadStream构造)

原型:stream.Readable.prototype

二级原型:stream.Stream.prototype

三级原型:eventes.EventEmitter.prototype

四级原型:Object.prototype

Stream对象都继承了EventEmitter
 

Stream 分类

节点中有四种基本流类型:Readable(可读),Writable(可写),Duplex(双向,可读可写)和Transform(变化)流。

  • 可读流是可以从中消费数据的源的抽象。 fs.createReadStream方法就是一个例子。
  • 可写流是可向其写入数据的目标的抽象。 fs.createWriteStream方法就是一个例子。
  • 双工流既可读取又可写入。 TCP套接字就是一个例子。
  • 转换流基本上是一种双工流,可用于在写入和读取数据时修改或转换数据。一个示例是zlib.createGzip流,该流使用gzip压缩数据。您可以将转换流视为函数,其中输入是可写流部分,输出是可读流部分。您可能还会听到称为“through streams”的变化流。

所有流都是EventEmitter的实例。它们发出可用于读取和写入数据的事件。但是,我们可以使用管道pipe方法以更简单的方式使用流数据。

 

pipe方法

常用代码:

stream1.pipe(stream2);

在这一简单的代码行中,我们将可读流的输出(数据源,作为可写流的输入),目的地传递到管道中。源必须是可读流,而目的地必须是可写流。当然,它们也可以都是双工/转换流。实际上,如果我们将管道传输到双工流中,则可以像在Linux中那样链式调用:

链式操作:

a.pipe(b)
  .pipe(c)
  .pipe(d);

// Which is equivalent to:
a.pipe(b);
b.pipe(c);
c.pipe(d);

// Which, in Linux, is equivalent to:
// $ a | b | c | d

pipe管道方法是消耗流的最简单方法。通常建议使用管道方法或使用带有事件的流,但避免将两者混用。通常,当您使用管道方法时,不需要使用事件,但是如果您需要以更多自定义方式使用流,则可以使用事件。

stream1.pipe(stream2) //pipe方法
// 事件方法
stream1.on('data', (chunk)=> { // stream1一有数据就塞给stream2
    stream2.write(chunk)
})  

stream1.on('end', ()=> {  // stream1停了,就停掉stream2
    stream2.end()
})

管道可以通过事件实现,两者等效。

以下是可与可读和可写流一起使用的重要事件和方法的列表:

 Readable StreamsWritable Streams
Eventsdata, end, error, close, readabledrain, finish, error, close, pipe, unpipe
Methodspipe(), unpipe(), wrap(), destroy()write(), destroy(), end()
 read(), unshift(), resume(), pause(), isPaused(), setEncoding()cork(), uncork(), setDefaultEncoding()

上面列表中的事件和方法以某种方式相关,因为它们通常一起使用。

可读流上最重要的事件是:

  • data事件,每当流将大量数据传递给使用者时发出
  • end事件,当没有更多数据要从流中使用时发出。

可写流上最重要的事件是:

  • drain干涸事件,这是可写流可以接收更多数据的信号。
  • finish完成事件,当所有数据都已刷新到基础系统时发出。

drain流干了事件:

表示可以加点水了,我们调用stream.write(chunk)的时候,可能会得到false,false的意思是你写太快,数据积压了。这个时候我们就不能再write了,要监听drain,等drain事件触发了,我们才能继续write。

finish事件:

调用stream.end()之后,而且缓冲区数据都已经传给底层系统之后,触发finish事件。

 

禁止态Paused和流动态Flowing

  • 默认处于paused态
  • 添加data事件监听,它就变为flowing态
  • 删掉data事件监听,它就变为paused态
  • pause() 可以将它变为paused
  • resume() 可以将它变为flowing

 

创建自己的流,给别人用

创建一个Writable Stream

const { Writable } = require("stream");

const outStream = new Writable({
  write(chunk, encoding, callback) {
    console.log(chunk.toString());
    callback();
  }
});

process.stdin.pipe(outStream);
// 保存文件为 writeable.js 然后用node运行
// 不管你输入什么,都会得到相同的结果

创建一个Readable Stream

const { Readable } = require("stream");

const inStream = new Readable();

inStream.push("ABCDEFGHIJKLM");
inStream.push("NOPQRSTUVWXYZ");

inStream.push(null); // No more data

inStream.pipe(process.stdout);
// 保存文件为 readable.js 然后用node运行
// 我们先把所以数据都push进去了,然后pipe

按需供给 

const inStream = new Readable({
  read(size) {
    this.push(String.fromCharCode(this.currentCharCode++));
    if (this.currentCharCode > 90) {
      this.push(null);
    }
  }
});

inStream.currentCharCode = 65;

inStream.pipe(process.stdout);
// 保存文件为readable2.js 然后用node运行
// 这次的数据是按需求供给的,对方调用read我们才会给一次数据

Duplex Stream

const { Duplex } = require("stream");

const inoutStream = new Duplex({
  write(chunk, encoding, callback) {
    console.log(chunk.toString());
    callback();
  },

  read(size) {
    this.push(String.fromCharCode(this.currentCharCode++));
    if (this.currentCharCode > 90) {
      this.push(null);
    }
  }
});

inoutStream.currentCharCode = 65;

process.stdin.pipe(inoutStream).pipe(process.stdout);

// write和read组合起来就是Duplex

Transform Stream

const { Transform } = require("stream");

const upperCaseTr = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

process.stdin.pipe(upperCaseTr).pipe(process.stdout);
// 输入小写输出大写

内置的 Transform Stream

const fs = require("fs");
const zlib = require("zlib");
const file = process.argv[2];

fs.createReadStream(file)
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream(file + ".gz"));
// 保存为gizp.js, 调用node gizp ./XX文件,得到gz压缩文件

const fs = require("fs");
const zlib = require("zlib");
const file = process.argv[2];

const { Transform } = require("stream");

const reportProgress = new Transform({
  transform(chunk, encoding, callback) {
    process.stdout.write(".");
    callback(null, chunk);
  }
});

fs.createReadStream(file)
  .pipe(zlib.createGzip())
  .pipe(reportProgress)
  .pipe(fs.createWriteStream(file + ".zz"))
  .on("finish", () => console.log("Done"));
// 可看见进度条,结束后打印done
const crypto = require("crypto");

// ..

fs.createReadStream(file)
  .pipe(crypto.createCipher("aes192", "123456"))
  .pipe(zlib.createGzip())
  .pipe(reportProgress)
  .pipe(fs.createWriteStream(file + ".zz"))
  .on("finish", () => console.log("Done"));
// 还可以在压缩前先加密文件

 

数据流中的积压问题

背压Back Pressure(node.js官方解释

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值