nodeJs中stream流基本概念与常见api

本节主要介绍 stream 的基本概念,以及常见的使用方式 。

本节以介绍基本概念为主,其中也会演示部分代码,和 stream 相关的 API 可不必深究,能通过代码看懂语义即可。后续章节会详细介绍这些 API 的原理和使用。

开始

本节主要内容有:

什么是 stream
为何要使用 stream
stream 流转的过程
stream 的常见使用场景
什么是 stream

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

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

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

为何要使用 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 的概念和使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值