Stream在node.js中是一个抽象的接口,基于EventEmitter,也是一种Buffer的高级封装,用来处理流数据。流模块便是提供各种API让我们可以很简单的使用Stream。

流分为四种类型,如下所示:

  • Readable,可读流
  • Writable,可写流
  • Duplex,读写流
  • Transform,扩展的Duplex,可修改写入的数据

1、Readable可读流

通过stream.Readable可创建一个可读流,它有两种模式:暂停和流动。

在流动模式下,将自动从下游系统读取数据并使用data事件输出;暂停模式下,必须显示调用stream.read()方法读取数据,并触发data事件。

所有的可读流最开始都是暂停模式,可以通过以下方法切换到流动模式:

  • 监听'data'事件
  • 调用stream.resume()方法
  • 调用stream.pipe()方法将数据输出到一个可写流Writable

同样地,也可以切换到暂停模式,有两种方法:

  • 如果没有设置pipe目标,调用stream.pause()方法即可。
  • 如果设置了pipe目标,则需要移除所有的data监听和调用stream.unpipe()方法

在Readable对象中有一个_readableSate的对象,通过该对象可以得知流当前处于什么模式,如下所示:

  • readable._readableState.flowing = null,没有数据消费者,流不产生数据
  • readable._readableState.flowing = true,处于流动模式
  • readable._readableState.flowing = false,处于暂停模式

为什么使用流取数据

对于小文件,使用fs.readFile()方法读取数据更方便,但需要读取大文件的时候,比如几G大小的文件,使用该方法将消耗大量的内存,甚至使程序崩溃。这种情况下,使用流来处理是更合适的,采用分段读取,便不会造成内存的'爆仓'问题。

data事件

在stream提供数据块给消费者时触发,有可能是切换到流动模式的时候,也有可能是调用readable.read() 方法且有有效数据块的时候,使用如下所示:

  1. const fs = require('fs');
  2. const rs = fs.createReadStream('./appbak.js');
  3. var chunkArr = [],
  4. chunkLen = 0;
  5. rs.on('data',(chunk)=>{
  6. chunkArr.push(chunk);
  7. chunkLen+=chunk.length;
  8. });
  9. rs.on('end',(chunk)=>{
  10. console.log(Buffer.concat(chunkArr,chunkLen).toString());
  11. });

readable事件

当流中有可用数据能被读取时触发,分为两种,新的可用的数据和到达流的末尾,前者stream.read()方法返回可用数据,后者返回null,如下所示:

  1. const rs = fs.createReadStream('./appbak.js');
  2. var chunkArr = [],
  3. chunkLen = 0;
  4. rs.on('readable',()=>{
  5. var chunk = null;
  6. //这里需要判断是否到了流的末尾
  7. if((chunk = rs.read()) !== null){
  8. chunkArr.push(chunk);
  9. chunkLen+=chunk.length;
  10. }
  11. });
  12. rs.on('end',(chunk)=>{
  13. console.log(Buffer.concat(chunkArr,chunkLen).toString());
  14. });

pause和resume方法

stream.pause()方法让流进入暂停模式,并停止'data'事件触发,stream.resume()方法使流进入流动模式,并恢复'data'事件触发,也可以用来消费所有数据,如下所示:

  1. const rs = fs.createReadStream('./下载.png');
  2. rs.on('data',(chunk)=>{
  3. console.log(`接收到${chunk.length}字节数据...`);
  4. rs.pause();
  5. console.log(`数据接收将暂停1.5秒.`);
  6. setTimeout(()=>{
  7. rs.resume();
  8. },1000);
  9. });
  10. rs.on('end',(chunk)=>{
  11. console.log(`数据接收完毕`);
  12. });

pipe(destination[, options])方法

pipe()方法绑定一个可写流到可读流上,并自动切换到流动模式,将所有数据输出到可写流,以及做好了数据流的管理,不会发生数据丢失的问题,使用如下所示:

  1. const rs = fs.createReadStream('./app.js');
  2. rs.pipe(process.stdout);

以上介绍了多种可读流的数据消费的方法,但对于一个可读流,最好只选择其中的一种,推荐使用pipe()方法。

2、Writable可写流

所有的可写流都是基于stream.Writable类创建的,创建之后便可将数据写入该流中。

write(chunk[, encoding][, callback])方法

write()方法向可写流中写入数据,参数含义:

  • chunk,字符串或buffer
  • encoding,若chunk为字符串,则是chunk的编码
  • callback,当前chunk数据写入磁盘时的回调函数

该方法的返回值为布尔值,如果为false,则表示需要写入的数据块被缓存并且此时缓存的大小超出highWaterMark阀值,否则为true。

使用如下所示:

  1. const ws = fs.createWriteStream('./test.txt');
  2. ws.write('nihao','utf8',()=>{process.stdout.write('this chunk is flushed.');});
  3. ws.end('done.')

背压机制

如果可写流的写入速度跟不上可读流的读取速度,write方法添加的数据将被缓存,逐渐增多,导致占用大量内存。我们希望的是消耗一个数据,再去读取一个数据,这样内存就维持在一个水平上。如何做到这一点?可以利用write方法的返回值来判断可写流的缓存状态和'drain'事件,及时切换可读流的模式,如下所示:

  1. function copy(src,dest){
  2. src = path.resolve(src);
  3. dest = path.resolve(dest);
  4. const rs = fs.createReadStream(src);
  5. const ws = fs.createWriteStream(dest);
  6. console.log('正在复制中...');
  7. const stime = +new Date();
  8. rs.on('data',(chunk)=>{
  9. if(null === ws.write(chunk)){
  10. rs.pause();
  11. }
  12. });
  13. ws.on('drain',()=>{
  14. rs.resume();
  15. });
  16. rs.on('end',()=>{
  17. const etime = +new Date();
  18. console.log(`已完成,用时:${(etime-stime)/1000}秒`);
  19. ws.end();
  20. });
  21. function calcProgress(){
  22. }
  23. }
  24. copy('./CSS权威指南 第3版.pdf','./javascript.pdf');

drain事件

如果Writable.write()方法返回false,则drain事件将会被触发,上面的背压机制已经使用了该事件。

finish事件

在调用stream.end()方法之后且所有缓存区的数据都被写入到下游系统,就会触发该事件,如下所示:

  1. const ws = fs.createWriteStream('./alphabet.txt');
  2. const alphabetStr = 'abcdefghijklmnopqrstuvwxyz';
  3. ws.on('finish',()=>{
  4. console.log('done.');
  5. });
  6. for(let letter of alphabetStr.split()){
  7. ws.write(letter);
  8. }
  9. ws.end();//必须调用

end([chunk][, encoding][, callback])方法

end()方法被调用之后,便不能再调用stream.write()方法写入数据,负责将抛出错误。

3、Duplex读写流

Duplex流同时实现了Readable与Writable类的接口,既是可读流,也是可写流。例如'zlib streams'、'crypto streams'、'TCP sockets'等都是Duplex流。

4、Transform流

Duplex流的扩展,区别在于,Transform流自动将写入端的数据变换后添加到可读端。例如:'zlib streams'、'crypto streams'等都是Transform流。

5、四种流的实现

stream模块提供的API可以让我们很简单的实现流,该模块使用require('stream')引用,我们只要继承四种流中的一个基类 (stream.Writable, stream.Readable, stream.Duplex, or stream.Transform),然后实现它的接口就可以了,需要实现的接口如下所示:

Use-case Class Method(s) to implement
Reading only Readable _read
Writing only Writable _write, _writev
Reading and writing Duplex _read, _write, _writev
Operate on written data, then read the result Transform _transform, _flush

Readable流实现

如上所示,我们只要继承Readable类并实现_read接口即可,,如下所示:

  1. const Readable = require('stream').Readable;
  2. const util = require('util');
  3. const alphabetArr = 'abcdefghijklmnopqrstuvwxyz'.split();
  4. /*function AbReadable(){
  5. if(!this instanceof AbReadable){
  6. return new AbReadable();
  7. }
  8. Readable.call(this);
  9. }
  10. util.inherits(AbReadable,Readable);
  11. AbReadable.prototype._read = function(){
  12. if(!alphabetArr.length){
  13. this.push(null);
  14. }else{
  15. this.push(alphabetArr.shift());
  16. }
  17. };
  18. const abReadable = new AbReadable();
  19. abReadable.pipe(process.stdout);*/
  20. /*class AbReadable extends Readable{
  21. constructor(){
  22. super();
  23. }
  24. _read(){
  25. if(!alphabetArr.length){
  26. this.push(null);
  27. }else{
  28. this.push(alphabetArr.shift());
  29. }
  30. }
  31. }
  32. const abReadable = new AbReadable();
  33. abReadable.pipe(process.stdout);*/
  34. /*const abReadable = new Readable({
  35. read(){
  36. if(!alphabetArr.length){
  37. this.push(null);
  38. }else{
  39. this.push(alphabetArr.shift());
  40. }
  41. }
  42. });
  43. abReadable.pipe(process.stdout);*/
  44. const abReadable = Readable();
  45. abReadable._read = function(){
  46. if (!alphabetArr.length) {
  47. this.push(null);
  48. } else {
  49. this.push(alphabetArr.shift());
  50. }
  51. }
  52. abReadable.pipe(process.stdout);

以上代码使用了四种方法创建一个Readable可读流,必须实现_read()方法,以及用到了readable.push()方法,该方法的作用是将指定的数据添加到读取队列。

Writable流实现

我们只要继承Writable类并实现_write或_writev接口,如下所示(只使用两种方法):

  1. /*class MyWritable extends Writable{
  2. constructor(){
  3. super();
  4. }
  5. _write(chunk,encoding,callback){
  6. process.stdout.write(chunk);
  7. callback();
  8. }
  9. }
  10. const myWritable = new MyWritable();*/
  11. const myWritable = new Writable({
  12. write(chunk,encoding,callback){
  13. process.stdout.write(chunk);
  14. callback();
  15. }
  16. });
  17. myWritable.on('finish',()=>{
  18. process.stdout.write('done');
  19. })
  20. myWritable.write('a');
  21. myWritable.write('b');
  22. myWritable.write('c');
  23. myWritable.end();

Duplex流实现

实现Duplex流,需要继承Duplex类,并实现_read和_write接口,如下所示:

  1. class MyDuplex extends Duplex{
  2. constructor(){
  3. super();
  4. this.source = [];
  5. }
  6. _read(){
  7. if (!this.source.length) {
  8. this.push(null);
  9. } else {
  10. this.push(this.source.shift());
  11. }
  12. }
  13. _write(chunk,encoding,cb){
  14. this.source.push(chunk);
  15. cb();
  16. }
  17. }
  18. const myDuplex = new MyDuplex();
  19. myDuplex.on('finish',()=>{
  20. process.stdout.write('write done.')
  21. });
  22. myDuplex.on('end',()=>{
  23. process.stdout.write('read done.')
  24. });
  25. myDuplex.write('\na\n');
  26. myDuplex.write('c\n');
  27. myDuplex.end('b\n');
  28. myDuplex.pipe(process.stdout);

上面的代码实现了_read()方法,可作为可读流来使用,同时实现了_write()方法,又可作为可写流来使用。

Transform流实现

实现Transform流,需要继承Transform类,并实现_transform接口,如下所示:

  1. class MyTransform extends Transform{
  2. constructor(){
  3. super();
  4. }
  5. _transform(chunk, encoding, callback){
  6. chunk = (chunk+'').toUpperCase();
  7. callback(null,chunk);
  8. }
  9. }
  10. const myTransform = new MyTransform();
  11. myTransform.write('hello world!');
  12. myTransform.end();
  13. myTransform.pipe(process.stdout);

上面代码中的_transform()方法,其第一个参数,要么为error,要么为null,第二个参数将被自动转发给readable.push()方法,因此该方法也可以使用如下写法:

  1. _transform(chunk, encoding, callback){
  2. chunk = (chunk+'').toUpperCase()
  3. this.push(chunk)
  4. callback();
  5. }

Object Mode流实现

我们知道流中的数据默认都是Buffer类型,可读流的数据进入流中便被转换成buffer,然后被消耗,可写流写入数据时,底层调用也将其转化为buffer。但将构造函数的objectMode选择设置为true,便可产生原样的数据,如下所示:

  1. const rs = Readable();
  2. rs.push('a');
  3. rs.push('b');
  4. rs.push(null);
  5. rs.on('data',(chunk)=>{console.log(chunk);});//<Buffer 61>与<Buffer 62>
  6. const rs1 = Readable({objectMode:!0});
  7. rs1.push('a');
  8. rs1.push('b');
  9. rs1.push(null);
  10. rs1.on('data',(chunk)=>{console.log(chunk);});//a与b

下面利用Transform流实现一个简单的CSS压缩工具,如下所示:

  1. function minify(src,dest){
  2. const transform = new Transform({
  3. transform(chunk,encoding,cb){
  4. cb(null,(chunk.toString()).replace(/[\s\r\n\t]/g,''));
  5. }
  6. });
  7. fs.createReadStream(src,{encoding:'utf8'}).pipe(transform).pipe(fs.createWriteStream(dest));
  8. }
  9. minify('./reset.css','./reset.min.css');

Node.js:理解stream的更多相关文章

  1. Node.js:Stream(流)

    Stream 是一个抽象接口,Node 中有很多对象实现了这个接口.例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出). Node.js,Str ...

  2. 理解 Node.js 中 Stream(流)

    Stream(流) 是 Node.js 中处理流式数据的抽象接口. stream 模块用于构建实现了流接口的对象. Node.js 提供了多种流对象. 例如,对 HTTP 服务器的request请求和 ...

  3. Node.js数据流Stream之Readable流和Writable流

    一.前传 Stream在很多语言都会有,当然Node.js也不例外.数据流是可读.可写.或即可读又可写的内存结构.Node.js中主要包括Readable.Writable.Duplex(双工)和Tr ...

  4. Node.js 【Stream之笔记】

    从Node.js API文档中可知, 'A stream is an abstract interface implemented by various objects in Node. For ex ...

  5. node.js中stream流中可读流和可写流的使用

    node.js中的流 stream 是处理流式数据的抽象接口.node.js 提供了很多流对象,像http中的request和response,和 process.stdout 都是流的实例. 流可以 ...

  6. 【Node.js】Stream(流)的学习笔记

    最近学习使用Node.js创建http proxy server,少不了要跟Stream打交道.昨天开始查阅一些资料,多少有了一些粗浅了解.整理在这里,供学习之用. 从Node.js API文档中可知 ...

  7. Node.js理解

    JavaScript单线程的误解 在我接触JavaScript(无论浏览器还是NodeJS)的时间里,总是遇到有朋友有多线程的需求.而在NodeJS方面,有朋友甚至直接说到,NodeJS是单线程的,无 ...

  8. Node.js数据流Stream之Duplex流和Transform流

    Duplex流一个很好的例子是TCP套接字连接.需要实现_read(size)和_Write(data,encoding,callback)方法. var stream = require('stre ...

  9. 【node.js】Stream(流)

    Stream 有四种流类型: Readable - 可读操作. Writable - 可写操作. Duplex - 可读可写操作. Transform - 操作被写入数据,然后读出结果. 所有的 St ...

  10. Node.js 使用Stream的pipe(管道)方法实现文件复制

    Stream模块有一个pipe方法,可以将两个流串起来,实现所有的数据自动从Readable流进入Writable流 "use strict"; const fs = requir ...

随机推荐

  1. node服务的监控预警系统架构

    需求背景 目前node端的服务逐渐成熟,在不少公司内部也开始承担业务处理或者视图渲染工作.不同于个人开发的简单服务器,企业级的node服务要求更为苛刻: 高稳定性.高可靠性.鲁棒性以及直观的监控和报警 ...

  2. 渗透测试工具BurpSuite做网站的安全测试(基础版)

    渗透测试工具BurpSuite做网站的安全测试(基础版) 版权声明:本文为博主原创文章,未经博主允许不得转载. 学习网址: https://t0data.gitbooks.io/burpsuite/c ...

  3. Centos 下 mysql root 密码重置

    重置mysql密码的方法有很多,官网也提供了很方便的快捷操作办法,可参考资料 resetting permissions .本文重置密码的具体步骤如下: 一.停止MySQL(如果处于运行状态) #se ...

  4. UWP开发之Mvvmlight实践八:为什么事件注销处理要写在OnNavigatingFrom中

    前一段开发UWP应用的时候因为系统返回按钮事件(SystemNavigationManager.GetForCurrentView().BackRequested)浪费了不少时间.现象就是在手机版的详 ...

  5. 为IEnumerable<T>添加RemoveAll<IEnumerable<T>>扩展方法--高性能篇

    最近写代码,遇到一个问题,微软基于List<T>自带的方法是public bool Remove(T item);,可是有时候我们可能会用到诸如RemoveAll<IEnumerab ...

  6. 获取微软原版“Windows 10 推送器(GWX)” 卸载工具

    背景: 随着Windows 10 免费更新的结束,针对之前提供推送通知的工具(以下简称GWX)来说使命已经结束,假设您还未将Windows 8.1 和Windows 7 更新到Windows 10 的 ...

  7. Javascript实现HashTable类

    散列算法可以尽快在数据结构中找出指定的一个值,因为可以通过Hash算法求出值的所在位置,存储和插入的时候都按照Hash算法放到指定位置. <script> function HashTab ...

  8. [原]分享一下我和MongoDB与Redis那些事

    缘起:来自于我在近期一个项目上遇到的问题,在Segmentfault上发表了提问 知识背景: 对不是很熟悉MongoDB和Redis的同学做一下介绍. 1.MongoDB数组查询:MongoDB自带L ...

  9. javascript动画系列第一篇——模拟拖拽

    × 目录 [1]原理介绍 [2]代码实现 [3]代码优化[4]拖拽冲突[5]IE兼容 前面的话 从本文开始,介绍javascript动画系列.javascript本身是具有原生拖放功能的,但是由于兼容 ...

  10. Springboot搭建web项目

    最近因为项目需要接触了springboot,然后被其快速零配置的特点惊呆了.关于springboot相关的介绍我就不赘述了,大家自行百度google. 一.pom配置 首先,建立一个maven项目,修 ...