什么是 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 对象,该对象可以监听 data
和 end
事件。
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 请求时会用到 req
和 res
,其实这两者都是 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原型和闭包》博客阅读量已超百万。同时也是慕课网热门讲师,在慕课网推出多门热门课程,学员评价极高:
热门评论
2222