什么是 stream

Stream 借鉴自 Unix 编程哲学中的 pipe。

Unix shell 命令中,管道式的操作 | 将上一个命令的输出作为下一个命令的输入。Node.js stream 中则是通过 .pip() 方法来进行的。

来看一个 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(8000);
  • stream 实现:
var http = require('http');
var fs = require('fs'); var server = http.createServer(function (req, res) {

var stream = fs.createReadStream(__dirname + '/data.txt');

stream.pipe(res);

});

server.listen(8000);

好处:

  • 代码更加简洁。
  • 可自由组合各种模块来处理数据。

stream 的种类

分五种:

  • readable
  • writable
  • duplex
  • transform
  • classic

readable

readable 类型的流产生的数据,可通过 .pip() 输送到能够消费(consume)流数据的地方,比如 writable,transform,duplex 类型的对象。

一个 readable stream 示例:

var Readable = require('stream').Readable;

var rs = new Readable;

rs.push('beep ');

rs.push('boop\n');

rs.push(null); rs.pipe(process.stdout);

运行结果:

$ node read0.js
beep boop

_read 方法与按需输出

上面 rs.push(null) 表示没有更多数据了。直接将数据塞入到 readable 流中,然后被缓冲起来,直到被消费(示例中消费方为 process.stdout,即输出到命令行。)。因为消费者有可能并不能立即消费这些内容,直接 push 数据后会消耗不必要的资源。

更好的做法是,让 readable 流只在消费者需要数据的时候再 push。这是通过定义能 raedable 对象定义 ._read 方法来完成的。

var Readable = require('stream').Readable;
var rs = Readable(); var c = 97;

rs._read = function () {

rs.push(String.fromCharCode(c++));

if (c > 'z'.charCodeAt(0)) rs.push(null);

}; rs.pipe(process.stdout);

运行结果:

$ node read1.js
abcdefghijklmnopqrstuvwxyz

这种方式下,定义了 readable 流产生数据的方法 ._read,但并没有马上执行并输出数据,而是在 process.stdout 读取时,才调用输出的。

_read 方法可动态接收一个可选的 size 参数,由消费方指定一次读取想要多少字节的数据,当然,_read 方法的实现中是可以忽略这个入参的。

下面的示例可证明 _read 方法是消费方调用的时候才执行的,而不是主动执行。

var Readable = require('stream').Readable;
var rs = Readable(); var c = 97 - 1; rs._read = function () {

if (c >= 'z'.charCodeAt(0)) return rs.push(null);
<span class="pl-c1">setTimeout</span>(<span class="pl-k">function</span> () {
<span class="pl-smi">rs</span>.<span class="pl-c1">push</span>(<span class="pl-c1">String</span>.<span class="pl-c1">fromCharCode</span>(<span class="pl-k">++</span>c));
}, <span class="pl-c1">100</span>);

};

rs.pipe(process.stdout);

process.on('exit', function () {

console.error('\n_read() called ' + (c - 97) + ' times');

});

process.stdout.on('error', process.exit);

输出任意数据

上面展示的是输出简单字符串,如果需要输出其他复杂数据,初始化时设置上正确的 objectMode 参数,Readable({ objectMode: true })

消费 readable 流产生的数据

一般情况下, 我们会将 readable 产生的数据直接传递给其他的消费方,比如 throughconcat-stream 创建的流对象。但直接操作来自 readable 的数据也是可以的。

process.stdin.on('readable', function () {
var buf = process.stdin.read();
console.dir(buf);
});

上面的示例代码中,监听了来自命令行的输入,有数据时 stdin readable 事件便会触发,然后可通过调用 read() 方法获取到数据。数据结束时 read() 返回 null 表示没有更多数据了。

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume0.js
<Buffer 61 62 63 0a>
<Buffer 64 65 66 0a>
<Buffer 67 68 69 0a>
null

同时 read() 方法支持传递一个 size 指定一次获取多少的字节(bytes)。比如一次只获取 3 字节的数据:

process.stdin.on('readable', function () {
var buf = process.stdin.read(3);
console.dir(buf);
});

这将导致我们获取到的数据是不完整的,因为指定了尺寸后,超出的部分是拿不到的。

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume1.js
<Buffer 61 62 63>
<Buffer 0a 64 65>
<Buffer 66 0a 67>

此时可通过调用 read(0) 告诉 Node.js 我们不希望丢弃超出的部分。这样超出的部分会在后面的读取中陆续输出。

process.stdin.on('readable', function () {
var buf = process.stdin.read(3);
console.dir(buf);
+ process.stdin.read(0);
});
$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume2.js
<Buffer 61 62 63>
<Buffer **0a** 64 65>
<Buffer 66 **0a** 67>
<Buffer 68 69 **0a**>

除了调用 read(0),还可调用 unshift() 方法将多余数据放回去。这样下次 read() 的时候数据都还在。比如一个解析换行的示例,遇到换行时将本行输出,剩下的数据塞回,以在下一行处理时使用。

var offset = 0;

process.stdin.on('readable', function () {

var buf = process.stdin.read();

if (!buf) return;

for (; offset < buf.length; offset++) {

if (buf[offset] === 0x0a) {

console.dir(buf.slice(0, offset).toString());

buf = buf.slice(offset + 1);

offset = 0;

process.stdin.unshift(buf);

return;

}

}

process.stdin.unshift(buf);

});
$ tail -n +50000 /usr/share/dict/american-english | head -n10 | node lines.js
'hearties'
'heartiest'
'heartily'
'heartiness'
'heartiness\'s'
'heartland'
'heartland\'s'
'heartlands'
'heartless'
'heartlessly'

writable 流

writable 流可作为 .pip() 的对象。

src.pipe(writableStream)

创建 writable 流

需要实现 ._write(chunk, enc, next) 方法,其中:

  • chunk 为接收到的数据
  • encopts.decodeStringfalse 且收到的数据这字符串时,它表示字符串的编码
  • next(err) 数据处理后的回调,可传递一个错误信息以表示数据处理失败

默认情况下,获取到的字符串数据会转为 Buffer,可设置 Writable({ decodeStrings: false }) 来获取字符串数据。

一个 writable 示例:

var Writable = require('stream').Writable;
var ws = Writable();
ws._write = function (chunk, enc, next) {
console.dir(chunk);
next();
}; process.stdin.pipe(ws);

向 writable 流写入数据

通过调用 writable 流的 write 方法来写入。

process.stdout.write('beep boop\n');

通过调用 end() 来结束数据的写入。

var fs = require('fs');
var ws = fs.createWriteStream('message.txt'); ws.write('beep '); setTimeout(function () {

ws.end('boop\n');

}, 1000);

duplex

双工类型的流,同时具有 writable 和 readable 流的功能。Node.js 内建的 zlib,TCP sockets 以及 crypto 都是双工类型的。

所以可对双工类型的流进行如下操作:

a.pip(b).pip(a)

transform

一种特殊类型的双工流,区别在于 transform 类型其输出是输入的转换。跟它的名字一样,这里面对数据进行一些转换后输出。比如,通过 zlib.createGzip 来对数据进行 gzip 的压缩。有时候也将这种类型的流称为 through steam

classic stream

这里指使用旧版 api 的流。当一个流身上绑定了 data 事件的监听时,便会回退为经典旧版的流。

classic readable stream

当有数据时它会派发 data 事件,数据输出结束时派发 end 事件给消费者。

.pipe() 通过检查 stream.readable 以判断该流是否是 readable 类型。

classic readable 流的创建

一个 classic readable 流的创建示例:

var Stream = require('stream');
var stream = new Stream;
stream.readable = true; var c = 64;

var iv = setInterval(function () {

if (++c >= 75) {

clearInterval(iv);

stream.emit('end');

}

else stream.emit('data', String.fromCharCode(c));

}, 100); stream.pipe(process.stdout);

从 classic readable 流读取数据

数据读取是通过监听流上的 dataend 事件。

一个从 classic readable 流读取数据的示例:

process.stdin.on('data', function (buf) {
console.log(buf);
});
process.stdin.on('end', function () {
console.log('__END__');
});

一般不建议通过这种方式来操作,一旦给流绑定 data 事件处理器,即回退到旧的 api 来使用流。如果真的有兼容操作旧版流的需求,应该通过 throughconcat-stream 来进行。

classic writable stream

只需要实现 .write(buf).end(buf) 及 .destroy() 方法即可,比较简单。

内建的流对象

总结

本质上,所有流都是 EventEmitter,通过事件可写入和读取数据。但通过新的 stream api,可方便地通过 .pipe() 方法来使用流而不是事件的方式。

使用 stream 可显著提高程序性能,特别是处理数据量大的情况下。它可以将数据分片处理,而不是粗暴地将数据看作一个整体。但掌握并熟练是需要一定时间练习的。

参考

Node.js 中的 stream的更多相关文章

  1. node.js中通过stream模块实现自定义流

    有些时候我们需要自定义一些流,来操作特殊对象,node.js中为我们提供了一些基本流类. 我们新创建的流类需要继承四个基本流类之一(stream.Writeable,stream.Readable,s ...

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

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

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

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

  4. Node.js:理解stream

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

  5. node.js中process进程的概念和child_process子进程模块的使用

    进程,你可以把它理解成一个正在运行的程序.node.js中每个应用程序都是进程类的实例对象. node.js中有一个 process 全局对象,通过它我们可以获取,运行该程序的用户,环境变量等信息. ...

  6. node.js中使用imagemagick进行图片裁剪压缩

    node.js中使用imagemagick进行图片裁剪压缩 安装imagemagick sudo apt-get install imagemagick or wget http://www.imag ...

  7. 学废了系列 - WebGL与Node.js中的Buffer

    WebGL 和 Node.js 中都有 Buffer 的使用,简单对比记录一下两个完全不相干的领域中 Buffer 异同,加强记忆. Buffer 是用来存储二进制数据的「缓冲区」,其本身的定义和用途 ...

  8. 在 Node.js 中处理大 JSON 文件

    在 Node.js 中处理大 JSON 文件 场景描述 问题一: 假设现在有一个场景,有一个大的 JSON 文件,需要读取每一条数据经过处理之后输出到一个文件或生成报表数据,怎么能够流式的每次读取一条 ...

  9. 在node.js中,使用基于ORM架构的Sequelize,操作mysql数据库之增删改查

    Sequelize是一个基于promise的关系型数据库ORM框架,这个库完全采用JavaScript开发并且能够用在Node.JS环境中,易于使用,支持多SQL方言(dialect),.它当前支持M ...

随机推荐

  1. 构建npm包出现"找不到node_modules"的问题

    目录结构 解决方案 先在微信开发者工具->详细->使用npm 1.cd到\WeChatProject\miniprogram文件夹 2.npm init 3.npm install 4.n ...

  2. Sightseeing trip POJ - 1734 -Floyd 最小环

    POJ - 1734 思路 : Floyd 实质 dp ,优化掉了第三维. dp [ i ] [ j ] [ k ] 指的是前k个点优化后    i  ->  j   的最短路. 所以我们就可以 ...

  3. oracle主键和索引

    主键:能够唯一标识一条记录的字段为主键(亦或主码),不能重复的,不允许为空.作用:用来保证数据完整性个数:主键只能有一个 索引:作用:是提高查询排序的速度个数:一个表可以有多个索引 常用索引类型:No ...

  4. Pandas 1 表格数据类型DataFrame

    # -*- encoding:utf-8 -*- # Copyright (c) 2015 Shiye Inc. # All rights reserved. # # Author: ldq < ...

  5. Zathura: 轻巧好用的 PDF 查看器]

    [Zathura: 轻巧好用的 PDF 查看器](https://linuxtoy.org/archives/zathura.html) 这个文件很轻巧,且支持VIM方式的 快捷键

  6. 微信公众平台测试号 “微信登录失败,redirect_uri域名与后台配置不一致,错误代码10003”

    设置"网页授权获取用户基本信息" 点击"修改" 弹出"OAuth2.0网页授权",注意域名不加"https://"或&q ...

  7. 4.24Linux(4)

    2019-4-24 21:35:13 学完了Linux装python编译安装感觉有种控制电脑的感觉!感觉好爽!!!! 主要是Linux用习惯就感觉好爽!!! 越努力,越幸运!永远不要高估自己!! 等学 ...

  8. 【原创】XAF CriteriaOperator 使用方式汇总

    1.CriteriaPropertyEditor [EditorAlias(EditorAliases.CriteriaPropertyEditor)] [CriteriaOptions(" ...

  9. 样式布局与 BFC

    一.几类视图 内联视图:inline 单行 块级视图:block 换行,有高度 行内块级视图:inline-block 单行,有高度 二.几类布局 块级布局 换行,通过设置 margin 水平居中 & ...

  10. 为不具有change事件的html标签设置监听事件

    change事件会在文本内容或选项被更改时触发. 该事件仅适用于<input type="text">和<textarea>以及<select> ...