Node.js Stream-基础篇
Node.js Stream - 基础篇
邹斌 ·2016-07-08 11:51
背景
在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。譬如,shell通过管道|
连接各部分,其输入输出的规范是文本流。
在Node.js中,内置的Stream模块也实现了类似功能,各部分通过.pipe()
连接。
鉴于目前国内系统性介绍Stream的文章较少,而越来越多的开源工具都使用了Stream,本系列文章将从以下几方面来介绍相关内容:
- 流的基本类型,以及Stream模块的基本使用方法
- 流式处理与back pressure的工作原理
- 如何开发流式程序,包括对Gulp与Browserify的剖析,以及一个实战示例。
本文为系列文章的第一篇。
流的四种类型
Stream提供了以下四种类型的流:
var Stream = require('stream') var Readable = Stream.Readable
var Writable = Stream.Writable
var Duplex = Stream.Duplex
var Transform = Stream.Transform
使用Stream
可实现数据的流式处理,如:
var fs = require('fs')
// `fs.createReadStream`创建一个`Readable`对象以读取`bigFile`的内容,并输出到标准输出
// 如果使用`fs.readFile`则可能由于文件过大而失败
fs.createReadStream(bigFile).pipe(process.stdout)
Readable
创建可读流。
实例:流式消耗迭代器中的数据。
'use strict'
const Readable = require('stream').Readable class ToReadable extends Readable {
constructor(iterator) {
super()
this.iterator = iterator
} // 子类需要实现该方法
// 这是生产数据的逻辑
_read() {
const res = this.iterator.next()
if (res.done) {
// 数据源已枯竭,调用`push(null)`通知流
return this.push(null)
}
setTimeout(() => {
// 通过`push`方法将数据添加到流中
this.push(res.value + '\n')
}, 0)
}
} module.exports = ToReadable
实际使用时,new ToReadable(iterator)
会返回一个可读流,下游可以流式的消耗迭代器中的数据。
const iterator = function (limit) {
return {
next: function () {
if (limit--) {
return { done: false, value: limit + Math.random() }
}
return { done: true }
}
}
}(1e10) const readable = new ToReadable(iterator) // 监听`data`事件,一次获取一个数据
readable.on('data', data => process.stdout.write(data)) // 所有数据均已读完
readable.on('end', () => process.stdout.write('DONE'))
执行上述代码,将会有100亿个随机数源源不断地写进标准输出流。
创建可读流时,需要继承Readable
,并实现_read
方法。
_read
方法是从底层系统读取具体数据的逻辑,即生产数据的逻辑。- 在
_read
方法中,通过调用push(data)
将数据放入可读流中供下游消耗。 - 在
_read
方法中,可以同步调用push(data)
,也可以异步调用。 - 当全部数据都生产出来后,必须调用
push(null)
来结束可读流。 - 流一旦结束,便不能再调用
push(data)
添加数据。
可以通过监听data
事件的方式消耗可读流。
- 在首次监听其
data
事件后,readable
便会持续不断地调用_read()
,通过触发data
事件将数据输出。 - 第一次
data
事件会在下一个tick中触发,所以,可以安全地将数据输出前的逻辑放在事件监听后(同一个tick中)。 - 当数据全部被消耗时,会触发
end
事件。
上面的例子中,process.stdout
代表标准输出流,实际是一个可写流。下小节中介绍可写流的用法。
Writable
创建可写流。
前面通过继承的方式去创建一类可读流,这种方法也适用于创建一类可写流,只是需要实现的是_write(data, enc, next)
方法,而不是_read()
方法。
有些简单的情况下不需要创建一类流,而只是一个流对象,可以用如下方式去做:
const Writable = require('stream').Writable const writable = Writable()
// 实现`_write`方法
// 这是将数据写入底层的逻辑
writable._write = function (data, enc, next) {
// 将流中的数据写入底层
process.stdout.write(data.toString().toUpperCase())
// 写入完成时,调用`next()`方法通知流传入下一个数据
process.nextTick(next)
} // 所有数据均已写入底层
writable.on('finish', () => process.stdout.write('DONE')) // 将一个数据写入流中
writable.write('a' + '\n')
writable.write('b' + '\n')
writable.write('c' + '\n') // 再无数据写入流时,需要调用`end`方法
writable.end()
- 上游通过调用
writable.write(data)
将数据写入可写流中。write()
方法会调用_write()
将data
写入底层。 - 在
_write
中,当数据成功写入底层后,必须调用next(err)
告诉流开始处理下一个数据。 next
的调用既可以是同步的,也可以是异步的。- 上游必须调用
writable.end(data)
来结束可写流,data
是可选的。此后,不能再调用write
新增数据。 - 在
end
方法调用后,当所有底层的写操作均完成时,会触发finish
事件。
Duplex
创建可读可写流。
Duplex
实际上就是继承了Readable
和Writable
的一类流。
所以,一个Duplex
对象既可当成可读流来使用(需要实现_read
方法),也可当成可写流来使用(需要实现_write
方法)。
var Duplex = require('stream').Duplex var duplex = Duplex() // 可读端底层读取逻辑
duplex._read = function () {
this._readNum = this._readNum || 0
if (this._readNum > 1) {
this.push(null)
} else {
this.push('' + (this._readNum++))
}
} // 可写端底层写逻辑
duplex._write = function (buf, enc, next) {
// a, b
process.stdout.write('_write ' + buf.toString() + '\n')
next()
} // 0, 1
duplex.on('data', data => console.log('ondata', data.toString())) duplex.write('a')
duplex.write('b') duplex.end()
上面的代码中实现了_read
方法,所以可以监听data
事件来消耗Duplex
产生的数据。
同时,又实现了_write
方法,可作为下游去消耗数据。
因为它既可读又可写,所以称它有两端:可写端和可读端。
可写端的接口与Writable
一致,作为下游来使用;可读端的接口与Readable
一致,作为上游来使用。
Transform
在上面的例子中,可读流中的数据(0, 1)与可写流中的数据('a', 'b')是隔离开的,但在Transform
中可写端写入的数据经变换后会自动添加到可读端。Tranform
继承自Duplex
,并已经实现了_read
和_write
方法,同时要求用户实现一个_transform
方法。
'use strict' const Transform = require('stream').Transform class Rotate extends Transform {
constructor(n) {
super()
// 将字母旋转`n`个位置
this.offset = (n || 13) % 26
} // 将可写端写入的数据变换后添加到可读端
_transform(buf, enc, next) {
var res = buf.toString().split('').map(c => {
var code = c.charCodeAt(0)
if (c >= 'a' && c <= 'z') {
code += this.offset
if (code > 'z'.charCodeAt(0)) {
code -= 26
}
} else if (c >= 'A' && c <= 'Z') {
code += this.offset
if (code > 'Z'.charCodeAt(0)) {
code -= 26
}
}
return String.fromCharCode(code)
}).join('') // 调用push方法将变换后的数据添加到可读端
this.push(res)
// 调用next方法准备处理下一个
next()
} } var transform = new Rotate(3)
transform.on('data', data => process.stdout.write(data))
transform.write('hello, ')
transform.write('world!')
transform.end() // khoor, zruog!
objectMode
前面几节的例子中,经常看到调用data.toString()
。这个toString()
的调用是必需的吗?
本节介绍完如何控制流中的数据类型后,自然就有了答案。
在shell中,用管道(|
)连接上下游。上游输出的是文本流(标准输出流),下游输入的也是文本流(标准输入流)。在本文介绍的流中,默认也是如此。
对于可读流来说,push(data)
时,data
只能是String
或Buffer
类型,而消耗时data
事件输出的数据都是Buffer
类型。对于可写流来说,write(data)
时,data
只能是String
或Buffer
类型,_write(data)
调用时传进来的data
都是Buffer
类型。
也就是说,流中的数据默认情况下都是Buffer
类型。产生的数据一放入流中,便转成Buffer
被消耗;写入的数据在传给底层写逻辑时,也被转成Buffer
类型。
但每个构造函数都接收一个配置对象,有一个objectMode
的选项,一旦设置为true
,就能出现“种瓜得瓜,种豆得豆”的效果。
Readable
未设置objectMode
时:
const Readable = require('stream').Readable const readable = Readable() readable.push('a')
readable.push('b')
readable.push(null) readable.on('data', data => console.log(data))
输出:
<Buffer 61>
<Buffer 62>
Readable
设置objectMode
后:
const Readable = require('stream').Readable const readable = Readable({ objectMode: true }) readable.push('a')
readable.push('b')
readable.push({})
readable.push(null) readable.on('data', data => console.log(data))
输出:
a
b
{}
可见,设置objectMode
后,push(data)
的数据被原样地输出了。此时,可以生产任意类型的数据。
预告
Stream系列共三篇文章:
- 第一部分:基础篇,介绍Stream接口的基本使用。
- 第二部分:进阶篇,重点剖析Stream底层如何支持流式数据处理,及其back pressure机制。
- 第三部分:实战篇。介绍如何使用Stream进行程序设计。从Browserify和Gulp总结出两种设计模式,并基于Stream构建一个为Git仓库自动生成changelog的应用作为示例。
参考文献
Node.js Stream-基础篇的更多相关文章
- Node.js Stream - 实战篇
邹斌 ·2016-07-22 11:04 背景 前面两篇(基础篇和进阶篇)主要介绍流的基本用法和原理,本篇从应用的角度,介绍如何使用管道进行程序设计,主要内容包括: 管道的概念 Browserify的 ...
- node.js学习(二)--Node.js控制台(REPL)&&Node.js的基础和语法
1.1.2 Node.js控制台(REPL) Node.js也有自己的虚拟的运行环境:REPL. 我们可以使用它来执行任何的Node.js或者javascript代码.还可以引入模块和使用文件系统. ...
- Node.js系列基础学习----安装,实现Hello World, REPL
Node.js基础学习 简介 简单的说 Node.js 就是运行在服务端的 JavaScript.Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台.Node.js是一 ...
- 9、Node.js Stream(流)
#########################################################################介绍Node.js Stream(流)Stream 是 ...
- Node.js npm基础安装配置&创建第一个VUE项目
使用之前,我们先来明白这几个东西是用来干什么的. node.js: 一种javascript的运行环境,能够使得javascript脱离浏览器运行.Node.js的出现,使得前后端使用同一种语言,统一 ...
- Node.js Stream-进阶篇
作者:美团点评技术团队链接:https://zhuanlan.zhihu.com/p/21681115来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 上篇(基础篇)主要 ...
- node.js入门基础
内容: 1.node.js介绍 2.node.js内置常用模块 3.node.js数据交互 一.node.js介绍 (1)node.js特点 与其他语言相比,有以下优点: 对象.语法和JavaScri ...
- Node.js系列基础学习-----回调函数,异步
Node.js基础学习 Node.js回调函数 Node.js异步编程的直接体现就是回调,异步编程依托回调来实现,但不是异步.回调函数在完成任务后就会被调用,Node有很多的回调函数,其所有的API都 ...
- Node.js stream 流学习
由于node.js 创建http 是这样的 http.createServer(function(request,response){}).listen(2000); 里面的request 就是rea ...
随机推荐
- spring类型自动转换——@InitBinder和Converter
spring有2种类型转换器,一种是propertyEditor,一种是Converter.虽然都是类型转换,但是还是有细微差别. 所以这里以一个例子的形式来分析一下这2种类型转换的使用场景和差别. ...
- Mysql日志解析
修改Mysql配置 Mysql配置地址为: C:\Program Files (x86)\MySQL\MySQL Server 5.5 如果无法修改可以把my.ini拷贝出来,修改完后,再拷贝回去! ...
- 报表软件FineReport如何连接SAP HANA
1. 环境搭建 1.1 环境准备 首先确认HANA Studio的环境是否允许工程进行NewFile的操作,不行的话要考虑更新Studio的版本. HANAStudio需要依赖Java jdk1.6或 ...
- Makefile 编写 tips
1.变量赋值 VARIABLE = value #在执行时扩展,允许递归扩展 VARIABLE := value #在定义时扩展 VARIABLE ?= value #只有在该变量为空时才设置该值 V ...
- 全国高校网安联赛Web专场~WriteUp
1.Sign 题目:Good Luck!flag{X-nuca@GoodLuck!} Flag直接写在题目上了,flag{X-nuca@GoodLuck!} 2.BaseCoding 提示:这是编码不 ...
- usb驱动开发之大结局
从usb总线的那个match函数usb_device_match()开始到现在,遇到了设备,遇到了设备驱动,遇到了接口,也遇到了接口驱动,期间还多次遇到usb_device_match(),又多次与它 ...
- 搭建TFS 2015 Build Agent环境(二)
在执行和安装配置的过程中,注意一定要使用管理员权限运行:ConfigureAgent.cmd 和RunAgent.cmd.配置的过程中,要注意几个内容:1.TFS地址不要写DefaultCollect ...
- 记一次排错,windows日志 模块 DLL C:\Windows\system32\inetsrv\aspnetcore.dll 未能加载。返回的数据为错误信息。
这个错误是在我本地开发环境,不是生产环境,如果是生产环境我就挂了....开发环境也痛苦啊,重装系统的话,我估计装系统+所有软件,少说也得1天..... 错误产生:重装IIS (尼玛,IIS总有一个小毛 ...
- [ASP.NET 5]终于解决:Unable to load DLL 'api-ms-win-core-localization-obsolete-l1-2-0.dll'
11月12日,惊喜地发现SqlClient(System.Data.SqlClient.dll)跨平台了(对应的nuget包包是runtime.unix.System.Data.SqlClient), ...
- 1122MySQL性能优化之 Nested Loop Join和Block Nested-Loop Join(BNL)
转自http://blog.itpub.net/22664653/viewspace-1692317/ 一 介绍 相信许多开发/DBA在使用MySQL的过程中,对于MySQL处理多表关联的方式或者说 ...