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() 方法且有有效数据块的时候,使用如下所示:

const fs = require('fs');

const rs = fs.createReadStream('./appbak.js');
var chunkArr = [],
chunkLen = 0;
rs.on('data',(chunk)=>{
chunkArr.push(chunk);
chunkLen+=chunk.length;
});
rs.on('end',(chunk)=>{
console.log(Buffer.concat(chunkArr,chunkLen).toString());
});

readable事件

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

const rs = fs.createReadStream('./appbak.js');
var chunkArr = [],
chunkLen = 0; rs.on('readable',()=>{
var chunk = null;
//这里需要判断是否到了流的末尾
if((chunk = rs.read()) !== null){
chunkArr.push(chunk);
chunkLen+=chunk.length;
}
});
rs.on('end',(chunk)=>{
console.log(Buffer.concat(chunkArr,chunkLen).toString());
});

pause和resume方法

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

const rs = fs.createReadStream('./下载.png');
rs.on('data',(chunk)=>{
console.log(`接收到${chunk.length}字节数据...`);
rs.pause();
console.log(`数据接收将暂停1.5秒.`);
setTimeout(()=>{
rs.resume();
},1000);
});
rs.on('end',(chunk)=>{
console.log(`数据接收完毕`);
});

pipe(destination[, options])方法

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

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

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

2、Writable可写流

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

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

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

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

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

使用如下所示:

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

背压机制

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

function copy(src,dest){
src = path.resolve(src);
dest = path.resolve(dest);
const rs = fs.createReadStream(src);
const ws = fs.createWriteStream(dest);
console.log('正在复制中...');
const stime = +new Date();
rs.on('data',(chunk)=>{
if(null === ws.write(chunk)){
rs.pause();
}
});
ws.on('drain',()=>{
rs.resume();
});
rs.on('end',()=>{
const etime = +new Date();
console.log(`已完成,用时:${(etime-stime)/1000}秒`);
ws.end();
});
function calcProgress(){ }
}
copy('./CSS权威指南 第3版.pdf','./javascript.pdf');

drain事件

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

finish事件

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

const ws = fs.createWriteStream('./alphabet.txt');
const alphabetStr = 'abcdefghijklmnopqrstuvwxyz';
ws.on('finish',()=>{
console.log('done.');
});
for(let letter of alphabetStr.split()){
ws.write(letter);
}
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接口即可,,如下所示:

const Readable = require('stream').Readable;
const util = require('util');
const alphabetArr = 'abcdefghijklmnopqrstuvwxyz'.split();
/*function AbReadable(){
if(!this instanceof AbReadable){
return new AbReadable();
}
Readable.call(this);
}
util.inherits(AbReadable,Readable);
AbReadable.prototype._read = function(){
if(!alphabetArr.length){
this.push(null);
}else{
this.push(alphabetArr.shift());
}
}; const abReadable = new AbReadable();
abReadable.pipe(process.stdout);*/ /*class AbReadable extends Readable{
constructor(){
super();
}
_read(){
if(!alphabetArr.length){
this.push(null);
}else{
this.push(alphabetArr.shift());
}
}
}
const abReadable = new AbReadable();
abReadable.pipe(process.stdout);*/ /*const abReadable = new Readable({
read(){
if(!alphabetArr.length){
this.push(null);
}else{
this.push(alphabetArr.shift());
}
}
});
abReadable.pipe(process.stdout);*/ const abReadable = Readable();
abReadable._read = function(){
if (!alphabetArr.length) {
this.push(null);
} else {
this.push(alphabetArr.shift());
}
}
abReadable.pipe(process.stdout);

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

Writable流实现

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

/*class MyWritable extends Writable{
constructor(){
super();
}
_write(chunk,encoding,callback){
process.stdout.write(chunk);
callback();
}
}
const myWritable = new MyWritable();*/
const myWritable = new Writable({
write(chunk,encoding,callback){
process.stdout.write(chunk);
callback();
}
});
myWritable.on('finish',()=>{
process.stdout.write('done');
})
myWritable.write('a');
myWritable.write('b');
myWritable.write('c');
myWritable.end();

Duplex流实现

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

class MyDuplex extends Duplex{
constructor(){
super();
this.source = [];
}
_read(){
if (!this.source.length) {
this.push(null);
} else {
this.push(this.source.shift());
}
}
_write(chunk,encoding,cb){
this.source.push(chunk);
cb();
}
} const myDuplex = new MyDuplex();
myDuplex.on('finish',()=>{
process.stdout.write('write done.')
});
myDuplex.on('end',()=>{
process.stdout.write('read done.')
});
myDuplex.write('\na\n');
myDuplex.write('c\n');
myDuplex.end('b\n');
myDuplex.pipe(process.stdout);

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

Transform流实现

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

class MyTransform extends Transform{
constructor(){
super();
}
_transform(chunk, encoding, callback){
chunk = (chunk+'').toUpperCase();
callback(null,chunk);
}
}
const myTransform = new MyTransform();
myTransform.write('hello world!');
myTransform.end();
myTransform.pipe(process.stdout);

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

_transform(chunk, encoding, callback){
chunk = (chunk+'').toUpperCase()
this.push(chunk)
callback();
}

Object Mode流实现

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

const rs = Readable();
rs.push('a');
rs.push('b');
rs.push(null);
rs.on('data',(chunk)=>{console.log(chunk);});//<Buffer 61>与<Buffer 62> const rs1 = Readable({objectMode:!0});
rs1.push('a');
rs1.push('b');
rs1.push(null);
rs1.on('data',(chunk)=>{console.log(chunk);});//a与b

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

function minify(src,dest){
const transform = new Transform({
transform(chunk,encoding,cb){
cb(null,(chunk.toString()).replace(/[\s\r\n\t]/g,''));
}
});
fs.createReadStream(src,{encoding:'utf8'}).pipe(transform).pipe(fs.createWriteStream(dest));
}
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. Java多线程基础学习(二)

    9. 线程安全/共享变量——同步 当多个线程用到同一个变量时,在修改值时存在同时修改的可能性,而此时该变量只能被赋值一次.这就会导致出现“线程安全”问题,这个被多个线程共用的变量称之为“共享变量”. ...

  2. C# 注册 Windows 热键

    闲扯: 前几日,一个朋友问我如何实现按 F1 键实现粘贴(Ctrl+V)功能,百度了一个方法,发给他,他看不懂(已经是 Boss 的曾经的码农),我就做了个Demo给他参考.今日得空,将 Demo 整 ...

  3. .NET Core系列 : 2 、project.json 这葫芦里卖的什么药

    .NET Core系列 : 1..NET Core 环境搭建和命令行CLI入门 介绍了.NET Core环境,本文介绍.NET Core中最重要的一个配置文件project.json的相关内容.我们可 ...

  4. 很多人很想知道怎么扫一扫二维码就能打开网站,就能添加联系人,就能链接wifi,今天说下这些格式,明天做个demo

    有些功能部分手机不能使用,网站,通讯录,wifi基本上每个手机都可以使用. 在看之前你可以扫一扫下面几个二维码先看看效果: 1.二维码生成 网址 (URL) 包含网址的 二维码生成 是大家平时最常接触 ...

  5. AutoMapper随笔记

    平台之大势何人能挡? 带着你的Net飞奔吧! http://www.cnblogs.com/dunitian/p/4822808.html#skill 先看效果:(完整Demo:https://git ...

  6. CSS Position 定位属性

    本篇文章主要介绍元素的Position属性,此属性可以设置元素在页面的定位方式. 目录 1. 介绍 position:介绍position的值以及辅助属性. 2. position 定位方式:介绍po ...

  7. EditText 基本用法

    title: EditText 基本用法 tags: EditText,编辑框,输入框 --- EditText介绍: EditText 在开发中也是经常用到的控件,也是一个比较必要的组件,可以说它是 ...

  8. android 两种实现计时器时分秒的实现,把时间放在你的手中~

    可能我们在开发中会时常用到计时器这玩意儿,比如在录像的时候,我们可能需要在右上角显示一个计时器.这个东西其实实现起来非常简单. 只需要用一个控件Chronometer,是的,就这么简单,我都不好意思讲 ...

  9. 编写自己的PHP MVC框架笔记

    1.MVC MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model).视图(View)和控制器(Controller). ...

  10. Kafka副本管理—— 为何去掉replica.lag.max.messages参数

    今天查看Kafka 0.10.0的官方文档,发现了这样一句话:Configuration parameter replica.lag.max.messages was removed. Partiti ...