为了账号安全,请及时绑定邮箱和手机立即绑定

如果你正学习或使用node.js,那么stream你一定要掌握!

标签:
Node.js

什么是 stream

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

英文叫 stream 中文叫“流”,都能很形象的表述出它的本质 —— 就是让数据流动起来。我们用桶和水来做比喻还算比较恰当(其实计算机中的概念,都是数学概念,都是抽象的,都无法完全用现实事务做比喻),如下图。数据从原来的 source 流向 dest ,要像水一样,慢慢的一点一点的通过一个管道流过去。给鱼缸换水、偷汽油,都能用得上。

图片描述

stream 并不是 nodejs 独有的概念,而是一个操作系统最基本的操作方式,只不过 nodejs 有 API 支持这种操作方式。linux 命令的 | 就是 stream ,因此所有 server 端语言都应该实现 stream 的 API 。

注:本文摘录《《两小时学会Node.js Stream》

为何要使用 stream

暂不管编程的原因,先分析一下上图中换水的例子。如果没有中间的管道,而是直接抱起 source 水桶往 dest 水桶中倒,那肯定得需要一个力量特别大的人(或者多个人)才能完成。而有了这个管道,小孩子都可以很轻松的完成换水,而且管道粗细都可以最终完成,只不过是时间长短的问题。即,有管道换水需要的力量消耗非常少,不用管道换水消耗力量很大,这个应该很好理解。

其实这里所说的“力量”,对应到计算机编程中就是硬件的性能,这包括 CPU 的运算能力,内存的存储能力,硬盘和网络的读写速度(硬盘存储能力暂不考虑)。将上面倒水的例子对应到一个计算机的场景中,例如在线看电影,source 就是服务器端的视频,dest 就是你自己的播放器(或者浏览器中的 flash 和 h5 video)。到这里大家应该一下子能明白,目前看电影的方式就是如同用管道换水一样,一点一点的从服务端将视频流动到本地播放器,一边流动一边播放,最后流动完了也播放完了。

那播放视频为何要使用这种方式?解决这个问题不妨考虑反证法,即不用管道和流动的方式,先从服务端加载完视频文件,然后再播放。这样导致最直观的问题就是,需要加载很长一段时间才能播放视频。其实这仅仅的表面现象,还有可能是视频加载过程中,因内存占用太多而导致系统卡顿或者崩溃。因为我们的网速、内存、CPU 运算速度都是有限的(而且还要多个程序共享使用),这个视频文件可能有几个 G 那么大。

再说一个更加直观的例子,先看下面的这段代码。语法上并没有什么问题,但是如果 data.txt 文件非常大,在响应大量用户的并发请求时,程序可能会消耗大量的内存,这样很可能会造成用户连接缓慢的问题。而且,如果并发请求过大,服务器内存开销也很大。

var http = require('http');
var fs = require('fs');
var path = require('path');

var server = http.createServer(function (req, res) {
    var fileName = path.resolve(__dirname, 'data.txt');
    fs.readFile(fileName, function (err, data) {
        res.end(data);
    });
});
server.listen(8000);

要解决这个问题很简单 —— 用 stream ,代码改造如下。即并不是把文件全部读取了再返回,而是一边读取一边返回,一点一点的把数据流动到客户端。

var http = require('http');
var fs = require('fs');
var path = require('path');

var server = http.createServer(function (req, res) {
    var fileName = path.resolve(__dirname, 'data.txt');
    var stream = fs.createReadStream(fileName);  // 这一行有改动
    stream.pipe(res); // 这一行有改动
});
server.listen(8000);

(注意,以上关于 stream 的 API 暂时没必要掌握,下文或者后面的章节会详细介绍)

最后总结一下,之所以用 stream ,是因为一次性读取、操作大文件,内存和网络是“吃不消”的,因此要让数据流动起来,一点一点的进行操作。这其实也符合算法中一个很重要的思想 —— 分而治之。

stream 流转的过程

从管道换水的例子可以看出,stream 包括 source,dest,还有中间的管道,下面将通过这三个方面来介绍 stream 的过程。其中比较关键的 API 有:

  • data 事件,用来监听 stream 数据的输入
  • end 事件,用来监听 stream 数据输入完成
  • fs.createReadStream 方法,返回一个文件读取的 stream 对象
  • fs.createWriteStream 方法,返回一个文件写入的 stream 对象
  • pipe 方法,用来做数据流转

这些 API 下文都会有介绍和代码演示,能通过代码看懂语义即可,暂时不用深究 API 细节,后续章节会有详细介绍。

source —— 从哪里来

stream 常见的来源方式主要有三种:

  • 从控制台输入
  • http 请求中的 request
  • 读取文件

运行如下代码,然后从控制台输入任何内容,都会被 data 事件监听到,process.stdin 就是一个 stream 对象。注意,data 就是 stream 用来监听数据传入的一个自定义函数,后续会大量用到这个方法

process.stdin.on('data', function (chunk) {
    console.log('stream by stdin', chunk.toString())
})

http 请求中的 request 输入可以参考如下代码片段(不能直接运行,后面章节会详解)。即客户端发起了 http 请求,服务端可以通过这种方式(注意也用到了 data 事件监听)来监听到数据的传入。这种 http 请求一般是一个 post 请求,上传数据。注意,end 用来监听 stream 数据传输完毕,一般和 data 共用

req.on('data', function (chunk) {
    // “一点一点”接收内容
    data += chunk.toString()
})
req.on('end', function () {
    // end 表示接收数据完成
})

读取文件是用 stream 如以下代码。fs.createReadStream(...) 可以返回一个读取文件的 stream 对象,该对象可以监听 dataend 事件。

var fs = require('fs')
var readStream = fs.createReadStream('./file1.txt')  // 读取文件的 Stream 对象

var length = 0
readStream.on('data', function (chunk) {
    length += chunk.toString().length
})
readStream.on('end', function () {
    console.log(length)
})

管道

以上 source 的三种代码示例中,都有一个共同点,就是对 stream 对象可以监听 data end 事件。 nodejs 中监听自定义事件要使用 .on 方法,例如 process.stdin.on('data', ...) req.on('data', ...) 。通过这种方式,能很直观的监听到 stream 数据的传入和结束。

根据上图管道倒水的例子,source 和 dest 之间有一个管道。我们已经介绍了 source ,在介绍 dest 之前先介绍一下这个管道 —— pipe ,其基本语法是 source.pipe(dest) ,source 上文已经介绍过三种类型,dest 下文会继续介绍三种类型,他们两者就是使用 pipe 进行连接,就是让数据从 source 流向 dest,就是管道。

dest —— 到哪里去

stream 常见输出方式主要有三种:

  • 输出到控制台
  • http 请求中的 response
  • 写入文件

上文讲解 source 时提到,process.stdin.on('data', ...) 可以监听控制台输入,而那仅仅是手动监听。如果让控制台输入这个 source 直接通过管道连接到控制台输入,即让数据从输入直接流向输出,使用如下代码。

process.stdin.pipe(process.stdout) // source.pipe(dest) 形式

nodejs 处理 http 请求时会用到 reqres ,其实这两者都是 stream 对象。其中 req 是 source ,可以 req.on('data', ...) 使用(上文已经演示过),res 是 dest ,用法如下。下面这段代码在本节文章一开始就介绍了,到这里大家应该明白,这是用 stream 的方式读取文件然后直接返回 http 请求。

var stream = fs.createReadStream(fileName);
stream.pipe(res); // source.pipe(dest) 形式

读取文件可以用 stream ,写入文件当然也可以用 stream ,如下代码。其中,fs.createWriteStream(...) 会返回一个写入文件的 stream 对象,即 dest 。这段代码,就是将一个文件中的内容,一点一点的流动到另外的文件中,完成复制功能。跟文章一开始管道换水的例子非常像。

var fs = require('fs')
var readStream = fs.createReadStream('./file1.txt')  // source
var writeStream = fs.createWriteStream('./file2.txt')  // dest
readStream.pipe(writeStream) // source.pipe(dest) 形式

stream 的常见使用场景

根据上文的介绍可以看出,stream 常见的应用场景是 http 请求和文件操作,后面的章节会根据这两个场景展开详细讲解。

总结来看,http 请求和文件操作都属于 IO ,即 stream 主要的应用场景就是处理 IO ,这就又回到了 stream 的本质 —— 由于一次性 IO 操作过大,硬件开销太多,影响软件运行效率,因此将 IO 分批分段操作,让数据一点一点的流动起来,直到操作完成。

总结

本节主要介绍了 stream 的基本概念和常用 API ,学完本节希望你能掌握:

  • stream 的基本概念,即 source -> 管道 -> dest 这个模型图。
  • 为何要用 stream ? —— 一次性操作 IO ,内存和网络开销太大。
  • source pipe dest 各种部分的常用 API ,要求能通过代码看懂语义。
  • stream 的常见应用场景 —— IO 操作。

很多其他教程和博客讲到 stream 都是先讲解那些晦涩难懂的概念,本教程反其道而行之,先不管那些概念,在了解基本概念之后,先去讲解实际应用,最后再总结 stream 的那些难懂的概念。因案例为主,引导你渐渐学会 stream 的概念和使用。

接下来,先讲解在 http 请求中对 stream 的使用。

更多精彩可以订阅《两小时学会Node.js Stream》

可扫描下方二维码查看下一节试读,也可直接订阅哦 ~

图片描述

作者介绍

双越 | 前端高级工程师,PMP,开源编辑器 wangEditor 作者。他编写的《深入理解JavaScript原型和闭包》博客阅读量已超百万。同时也是慕课网热门讲师,在慕课网推出多门热门课程,学员评价极高:

点击查看更多内容
36人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消