在编写代码时,我们应该有一些方法将程序像连接水管一样连接起来 -- 当我们需要获取一些数据时,可以去通过"拧"其他的部分来达到目的。这也应该是IO应有的方式。 -- Doug McIlroy. October 11, 1964

为什么应该使用stream?

在node中,I/O都是异步的,所以在和硬盘以及网络的交互过程中会涉及到传递回调函数的过程。你之前可能会写出这样的代码:

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);

上面的这段代码并没有什么问题,但是在每次请求时,我们都会把整个data.txt文件读入到内存中,然后再把结果返回给客户端。想想看,如果data.txt文件非常大,在响应大量用户的并发请求时,程序可能会消耗大量的内存,这样很可能会造成用户连接缓慢的问题。其次,上面的代码可能会造成很不好的用户体验,因为用户在接收到任何的内容之前首先需要等待程序将文件内容完全读入到内存中。所幸的是,(req,res) 参数都是流对象,这意味着我们可以使用一种更好的方法来实现上面的需求:

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);

在这里,.pipe()方法会自动帮助我们监听data和end事件。上面的这段代码不仅简洁,而且data.txt文件中每一小段数据都将源源不断的发送到客户端。

除此之外,使用.pipe()方法还有别的好处,比如说它可以自动控制后端压力,以便在客户端连接缓慢的时候node可以将尽可能少的缓存放到内存中。

认识NodeJS中的stream

流(stream)是 Node.js 中处理流式数据的抽象接口。·stream 模块用于构建实现了流接口的对象。

我们用到的很多核心模块都是stream的实例。 例如:http.clientRequest, process.stdout。

流可以是可读的、可写的、或者可读可写的。

所有的流都是 EventEmitter 的实例。

虽然我们平时开发过程中平常不会直接用到stream模块,但是也需要了解其运行机制。

对于想要实现自定义stream实例的开发者来说,就得好好研究stream的扩展API了,比如gulp的内部实现就大量用到了自定义的stream类型。

stream的类型

Node.js 中有四种基本的流类型:

  • Writable - 可写入数据的流(例如 fs.createWriteStream())。
  • Readable - 可读取数据的流(例如 fs.createReadStream())。
  • Duplex - 可读又可写的流(例如 net.Socket)。
  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())。

使用Stream可实现数据的流式处理,如:

var fs = require('fs')
// `fs.createReadStream`创建一个`Readable`对象以读取`bigFile`的内容,并输出到标准输出
// 如果使用`fs.readFile`则可能由于文件过大而失败
fs.createReadStream(bigFile).pipe(process.stdout)

Readable

Readable流可以产出数据,你可以将这些数据传送到一个writable,transform或者duplex流中,只需要调用pipe()方法:

创建个readable流

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

var rs = new Readable;
rs.push('beep ');
rs.push('boop\n');
rs.push(null); rs.pipe(process.stdout);

下面运行代码

$ node read.js
beep boop

在上面的代码中rs.push(null)的作用是告诉rs输出数据应该结束了。

需要注意的一点是我们在将数据输出到process.stdout之前已经将内容推送进readable流rs中,但是所有的数据依然是可写的。这是因为在你使用.push()将数据推进一个readable流中时,一直要到另一个东西来消耗数据之前,数据都会存在一个缓存中。然而,在更多的情况下,我们想要的是当需要数据时数据才会产生,以此来避免大量的缓存数据。

流式消耗迭代器中的数据

我们可以通过定义一个._read函数来实现按需推送数据:

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

一个writable流指的是只能流进不能流出的流:

src.pipe(writableStream)

创建一个writable流

只需要定义一个._write(chunk,enc,next)函数,你就可以将一个readable流的数据释放到其中:

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()

运行结果如下:

$ node 1.js
A
B
C
DONE
  • 上游通过调用writable.write(data)将数据写入可写流中。write()方法会调用_write()将data写入底层。
  • _write中,当数据成功写入底层后,必须调用next(err)告诉流开始处理下一个数据。
  • 在从一个readable流向一个writable流传数据的过程中,数据会自动被转换为Buffer对象,除非你在创建writable流的时候制定了decodeStrings参数为false:Writable({decodeStrings: false})
  • 如果你需要传递对象,需要指定objectMode参数为trueWritable({ objectMode: true })
  • 在end方法调用后,当所有底层的写操作均完成时,会触发finish事件。
  • 上游必须调用writable.end(data)来结束可写流,data是可选的。此后,不能再调用write新增数据。
  • next的调用既可以是同步的,也可以是异步的.

_write的参数:

  • 第一个参数,chunk表写进来的数据。
  • 第二个参数 enc 代表编码的字符串,但是只有在opts.decodeStringfalse的时候你才可以写一个字符串。
  • 第三个参数,next(err)是一个回调函数,使用这个回调函数你可以告诉数据消耗者可以写更多的数据。你可以有选择性的传递一个错误对象error,这时会在流实体上触发一个emit事件。

向一个writable流中写东西

如果你需要向一个writable流中写东西,只需要调用.write(data)即可。

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

为了告诉一个writable流你已经写完毕了,只需要调用.end()方法。你也可以使用.end(data)在结束前再写一些数据。

var fs = require('fs');
var ws = fs.createWriteStream('message.txt'); ws.write('beep '); setTimeout(function () {
ws.end('boop\n');
},1000);

运行结果如下所示:

$ node writing.js
$ cat message.txt
beep boop

如果你在创建writable流时指定了highWaterMark参数,那么当没有更多数据写入时,调用.write()方法将会返回false。如果你想要等待缓存情况,可以监听drain事件。

Duplex

Duplex流是一个可读也可写的流,就好像一个电话,可以接收也可以发送语音。一个rpc交换是一个duplex流的最好的例子。如果你看到过下面这样的代码:

a.pipe(b).pipe(a)

那么你需要处理的就是一个duplex流对象。

实现一个Duplex

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

Transform stream是Duplex stream的特例,也就是说,Transform stream也同时可读可写。跟Duplex stream的区别点在于,Transform stream的输出与输入是存在相关性的。

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()

执行结果如下:

$ node 1.js
khoor, zruog!

Tranform继承自Duplex,并已经实现了_read和_write方法,同时要求用户实现一个_transform方法。

相关链接

https://nodejs.org/api/stream.html

每天学点node系列-stream的更多相关文章

  1. 每天学点node系列-fs文件系统(续)

    理想如果不向现实做一点点屈服,那么理想也将归于尘土. 创建目录 异步创建目录 path<string> | <Buffer> | <URL> options < ...

  2. 每天学点node系列-http

    任何可以使用JavaScript来编写的应用,最终会由JavaScript编写.--Atwood's Law http模块概览 http模块主要用于创建http server服务,并且 支持更多特性 ...

  3. 每天学点node系列-fs文件系统

    好的代码像粥一样,都是用时间熬出来的. 概述 文件 I/O 是由简单封装的标准 POSIX 函数提供的. 通过 require('fs') 使用该模块. 所有文件系统操作都具有同步和异步的形式. 异步 ...

  4. 每天学点node系列-zlib

    永不放弃,永不放弃又有两个原则,第一个原则是永不放弃,第二个原则就是:当你想放弃时回头看第一个原则. 概览 做过web性能优化的同学,对性能优化大杀器gzip应该不陌生.浏览器向服务器发起资源请求,比 ...

  5. Flutter 即学即用系列博客——09 EventChannel 实现原生与 Flutter 通信(一)

    前言 紧接着上一篇,这一篇我们讲一下原生怎么给 Flutter 发信号,即原生-> Flutter 还是通过 Flutter 官网的 Example 来讲解. 案例 接着上一次,这一次我们让原生 ...

  6. 一步一步学Silverlight 2系列(16):数据与通信之JSON

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  7. 一步一步学Silverlight 2系列(13):数据与通信之WebRequest

    概述 Silverlight 2 Beta 1版本发布了,无论从Runtime还是Tools都给我们带来了很多的惊喜,如支持框架语言Visual Basic, Visual C#, IronRuby, ...

  8. 我教女朋友学编程html系列(7)—Html无序列表、自定义列表、有序列表及常用例子

    昨天写的那篇文章<我教女朋友学编程Html系列(6)—Html常用表单控件>,基本上有1000人左右看了,那边文章是我站在前人的肩膀上修改来的,添加了截图和说明,合并了例子,使之更容易被初 ...

  9. .Net程序员学用Oracle系列(1):导航目录

    本人从事基于 Oracle 的 .Net 企业级开发近三年,在此之前学习和使用的都是 (MS)SQL Server.未曾系统的了解过 Oracle,所以长时间感到各种不习惯.不方便.怪异和不解,常会遇 ...

随机推荐

  1. OpenCV 图像白平衡算法(相机自动白平衡)

    彩色相机内部有三个CCD电子耦合元件,分别用来感受红绿蓝三中颜色的光线,默认情况下,三个颜色的感光电路信号的放大比例是1:1:1的.在理想的拍摄环境下,纯白色的RGB分量按照1:1:1的比例放大之后, ...

  2. windows 的使用 —— 注册表(软件的安装和卸载)

    win + r(run):输入 regedit(register edit)进入: 1. 网络连接 比如一些 vpn 安装之后,会对网络连接进行一定的修改,这样在 vpn 工具删除之后,仍然无法消除修 ...

  3. 第一泰泽(Tizen)智能手机发布在俄罗斯

    请看下图: 这是韩国三星公司在俄罗斯境内公布的第一款泰泽(Tizen)智能手机(今年6月2日).这说明,Tizen操作系统没有死去. 在泰泽官网上将泰泽操作系统定义为:"The OS of ...

  4. 微软Insider Dev Tour 活动

    总述 Insider Dev Tour 将在6月全球29个城市中展开.这次活动是通过与微软全球MVPs和RD合作进行的,并涵盖了微软最新的开者方面的最新消息. 日期提示 2018年5月7日 : 暂无其 ...

  5. 1 min 数据查询 SQL 优化

    问题 前几天线上数据库 IOPS 飙升,一直居高不下,最近并没有升级.遂查看数据库正在执行的 SQL 语句,发现有个查询离线设备的语句极其缓慢. 探寻原因 SELECT o.* FROM ( SELE ...

  6. js错误界面

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  7. 在 Excel 中如何使用宏示例删除列表中的重复项

    概要:在 Microsoft Excel 中,可以创建宏来删除列表中的重复项.也可以创建宏来比较两个列表,并删除第二个列表中那些也出现在第一个(主)列表中的项目.如果您想将两个列表合并在一起,或者如果 ...

  8. NSLocalizedDescription=Request failed: unacceptable content-type: text/html 解决方法

    使用AFNetworking请求一个网站出现了以下错误 Error Domain=com.alamofire.error.serialization.response Code=- "Req ...

  9. Windows 10开发基础——VS2015 Update1新建UWP项目,XAML设计器无法加载的解决

    这次,我们来解决一个问题...在使用Visual Studio 2015 Update 1的时候,新建一个UWP的项目,XAML设计器就会崩,具体异常信息如下图: 解决方法如下:下面圈出的那个路径就按 ...

  10. Win8Metro(C#)数字图像处理--2.14Prewitt 边缘检测

    原文:Win8Metro(C#)数字图像处理--2.14Prewitt 边缘检测  [函数名称] 图像Prewitt边缘检测函数PrewittEdgeProcess(WriteableBitmap ...