说Node.js做后端开发,stream有必要了解下

论坛 期权论坛 期权     
程序员成长指北   2019-7-13 08:27   3419   0


[h1]什么是stream [/h1][h2]定义[/h2]流的英文
  1. stream
复制代码
,流(Stream)是一个抽象的数据接口,
  1. Node.js
复制代码
中很多对象都实现了流,流是
  1. EventEmitter
复制代码
对象的一个实例,总之它是会冒数据(以
  1. Buffer
复制代码
为单位),或者能够吸收数据的东西,它的本质就是让数据流动起来。可能看一张图会更直观:

水桶管道流转图注意:
  1. stream
复制代码
不是node.js独有的概念,而是一个操作系统最基本的操作方式,只不过node.js有API支持这种操作方式。linux命令的|就是
  1. stream
复制代码

[h1]为什么要学习stream [/h1][h2]视频播放例子[/h2]小伙伴们肯定都在线看过电影,对比定义中的图-
  1. 水桶管道流转图
复制代码
  1. source
复制代码
就是服务器端的视频,
  1. dest
复制代码
就是你自己的播放器(或者浏览器中的flash和h5 video)。大家想一下,看电影的方式就如同上面的图管道换水一样,一点点从服务端将视频流动到本地播放器,一边流动一边播放,最后流动完了也就播放完了。
说明:视频播放的这个例子,如果我们不使用管道和流动的方式,直接先从服务端加载完视频文件,然后再播放。会造成很多问题
  • 因内存占有太多而导致系统卡顿或者崩溃
  • 因为我们的网速 内存 cpu运算速度都是有限的,而且还要有多个程序共享使用,一个视频文件加载完可能有几个g那么大。
[h2]读取大文件data的例子[/h2]有一个这样的需求,想要读取大文件data的例子
使用文件读取
  1. const http = require('http');const fs = require('fs');const path = require('path');const server = http.createServer(function (req, res) {    const fileName = path.resolve(__dirname, 'data.txt');    fs.readFile(fileName, function (err, data) {        res.end(data);    });});server.listen(8000);
复制代码
使用文件读取这段代码语法上并没有什么问题,但是如果data.txt文件非常大的话,到了几百M,在响应大量用户并发请求的时候,程序可能会消耗大量的内存,这样可能造成用户连接缓慢的问题。而且并发请求过大的话,服务器内存开销也会很大。这时候我们来看一下用
  1. stream
复制代码
实现。
  1. const http = require('http');const fs = require('fs');const path = require('path');const server = http.createServer(function (req, res) {    const fileName = path.resolve(__dirname, 'data.txt');    let stream = fs.createReadStream(fileName);  // 这一行有改动    stream.pipe(res); // 这一行有改动});server.listen(8000);
复制代码
使用stream就可以不需要把文件全部读取了再返回,而是一边读取一边返回,数据通过管道流动给客户端,真的减轻了服务器的压力。
看了两个例子我想小伙伴们应该知道为
  1. 什么要使用stream
复制代码
了吧!因为一次性读取,操作大文件,内存和网络是吃不消的,因此要让数据流动起来,一点点的进行操作。
[h1]stream流转过程 [/h1]再次看这张
  1. 水桶管道流转图
复制代码


图中可以看出,
  1. stream
复制代码
整个流转过程包括source,dest,还有连接二者的管道pipe(stream的核心),分别介绍三者来带领大家搞懂stream流转过程。
[h2]stream从哪里来-soucre[/h2]
  1. stream
复制代码
的常见来源方式有三种:
  • 从控制台输入
    1. http
    复制代码
    请求中的
    1. request
    复制代码
  • 读取文件
这里先说一下
  1. 从控制台输入
复制代码
这种方式,2和3两种方式
  1. stream应用场景
复制代码
章节会有详细的讲解。
看一段
  1. process.stdin
复制代码
的代码
  1. process.stdin.on('data', function (chunk) {    console.log('stream by stdin', chunk)    console.log('stream by stdin', chunk.toString())})//控制台输入koalakoala后输出结果stream by stdin stream by stdin koalakoala
复制代码
运行上面代码:然后从控制台输入任何内容都会被
  1. data
复制代码
事件监听到,
  1. process.stdin
复制代码
就是一个
  1. stream
复制代码
对象,data
  1. stream
复制代码
对象用来监听数据传入的一个自定义函数,通过输出结果可看出
  1. process.stdin
复制代码
是一个stream对象。
说明:
  1. stream
复制代码
对象可以监听
  1. "data"
复制代码
,
  1. "end"
复制代码
,
  1. "opne"
复制代码
,
  1. "close"
复制代码
,
  1. "error"
复制代码
等事件。
  1. node.js
复制代码
中监听自定义事件使用
  1. .on
复制代码
方法,例如
  1. process.stdin.on(‘data’,…)
复制代码
,
  1. req.on(‘data’,…)
复制代码
,通过这种方式,能很直观的监听到
  1. stream
复制代码
数据的传入和结束
[h2]连接水桶的管道-pipe[/h2]从水桶管道流转图中可以看到,在
  1. source
复制代码
  1. dest
复制代码
之间有一个连接的管道
  1. pipe
复制代码
,它的基本语法是
  1. source.pipe(dest)
复制代码
  1. source
复制代码
  1. dest
复制代码
就是通过pipe连接,让数据从
  1. source
复制代码
流向了
  1. dest
复制代码

[h2]stream到哪里去-dest[/h2]stream的常见输出方式有三种:
  • 输出控制台
    1. http
    复制代码
    请求中的
    1. response
    复制代码
  • 写入文件
[h1]stream应用场景 [/h1]
  1. stream
复制代码
的应用场景主要就是处理
  1. IO
复制代码
操作,而
  1. http请求
复制代码
  1. 文件操作
复制代码
都属于
  1. IO
复制代码
操作。这里再提一下
  1. stream
复制代码
的本质——由于一次性
  1. IO
复制代码
操作过大,硬件开销太多,影响软件运行效率,因此将
  1. IO
复制代码
分批分段进行操作,让数据像水管一样流动起来,直到流动完成,也就是操作完成。下面对几个常用的应用场景分别进行介绍
[h2]介绍一个压力测试的小工具[/h2]一个对网络请求做压力测试的工具
  1. ab
复制代码
  1. ab
复制代码
全称
  1. Apache bench
复制代码
,是
  1. Apache
复制代码
自带的一个工具,因此使用
  1. ab
复制代码
必须要安装
  1. Apache
复制代码
。mac os 系统自带
  1. Apache
复制代码
  1. windows
复制代码
用户视自己的情况进行安装。运行
  1. ab
复制代码
之前先启动
  1. Apache
复制代码
  1. mac os
复制代码
启动方式是
  1. sudo apachectl start
复制代码

Apache bench对应参数的详细学习地址,有兴趣的可以看一下Apache bench对应参数的详细学习地址
介绍这个小工具的目的是对下面几个场景可以进行直观的测试,看出使用stream带来了哪些性能的提升。
[h2]get请求中应用stream[/h2]这样一个需求:
使用node.js实现一个http请求,读取data.txt文件,创建一个服务,监听8000端口,读取文件后返回给客户端,讲get请求的时候用一个常规文件读取与其做对比,请看下面的例子。
  • 常规使用文件读取返回给客户端response例子 ,文件命名为
    1. getTest1.js
    复制代码
  1. // getTest.jsconst http = require('http');const fs = require('fs');const path = require('path');const server = http.createServer(function (req, res) {    const method = req.method; // 获取请求方法    if (method === 'GET') { // get 请求方法判断        const fileName = path.resolve(__dirname, 'data.txt');        fs.readFile(fileName, function (err, data) {            res.end(data);        });    }});server.listen(8000);
复制代码
  • 使用stream返回给客户端response
    将上面代码做部分修改,文件命名为
    1. getTest2.js
    复制代码
  1. // getTest2.js// 主要展示改动的部分const server = http.createServer(function (req, res) {    const method = req.method; // 获取请求方法    if (method === 'GET') { // get 请求        const fileName = path.resolve(__dirname, 'data.txt');        let stream = fs.createReadStream(fileName);        stream.pipe(res); // 将 res 作为 stream 的 dest    }});server.listen(8000);
复制代码
对于下面get请求中使用stream的例子,会不会有些小伙伴提出质疑,难道response也是一个stream对象,是的没错,对于那张
  1. 水桶管道流转图
复制代码
,response就是一个dest。
虽然get请求中可以使用stream,但是相比直接file文件读取
  1. ·res.end(data)
复制代码
有什么好处呢?这时候我们刚才推荐的压力测试小工具就用到了。
  1. getTest1
复制代码
  1. getTest2
复制代码
两段代码,将
  1. data.txt
复制代码
内容增加大一些,使用
  1. ab
复制代码
工具进行测试,运行命令
  1. ab -n 100 -c 100 http://localhost:8000/
复制代码
,其中
  1. -n 100
复制代码
表示先后发送100次请求,
  1. -c 100
复制代码
表示一次性发送的请求数目为100个。对比结果分析使用stream后,有非常大的性能提升,小伙伴们可以自己实际操作看一下。
[h2]post中使用stream[/h2]一个通过post请求微信小程序的地址生成二维码的需求。
  1. /** 微信生成二维码接口* params src 微信url / 其他图片请求链接* params localFilePath: 本地路径* params data: 微信请求参数* */const downloadFile=async (src, localFilePath, data)=> {    try{        const ws = fs.createWriteStream(localFilePath);        return new Promise((resolve, reject) => {            ws.on('finish', () => {                resolve(localFilePath);            });            if (data) {                request({                    method: 'POST',                    uri: src,                    json: true,                    body: data                }).pipe(ws);            } else {                request(src).pipe(ws);            }        });    }catch (e){        logger.error('wxdownloadFile error: ',e);        throw e;    }}
复制代码
看这段使用了stream的代码,为本地文件对应的路径创建一个stream对象,然后直接
  1. .pipe(ws)
复制代码
,将post请求的数据流转到这个本地文件中,这种stream的应用在node后端开发过程中还是比较常用的。
[h2]post与get使用stream总结[/h2]request和reponse一样,都是stream对象,可以使用stream的特性,二者的区别在于,我们再看一下
  1. 水桶管道流转图
复制代码


request是source类型,是图中的源头,而response是dest类型,是图中的目的地。
[h2]在文件操作中使用stream[/h2]一个文件拷贝的例子
  1. const fs = require('fs')const path = require('path')// 两个文件名const fileName1 = path.resolve(__dirname, 'data.txt')const fileName2 = path.resolve(__dirname, 'data-bak.txt')// 读取文件的 stream 对象const readStream = fs.createReadStream(fileName1)// 写入文件的 stream 对象const writeStream = fs.createWriteStream(fileName2)// 通过 pipe执行拷贝,数据流转readStream.pipe(writeStream)// 数据读取完成监听,即拷贝完成readStream.on('end', function () {    console.log('拷贝完成')})
复制代码
看了这段代码,发现是不是拷贝好像很简单,创建一个可读数据流
  1. readStream
复制代码
,一个可写数据流
  1. writeStream
复制代码
,然后直接通过
  1. pipe
复制代码
管道把数据流转过去。这种使用stream的拷贝相比存文件的读写实现拷贝,性能要增加很多,所以小伙伴们在遇到文件操作的需求的时候,尽量先评估一下是否需要使用
  1. stream
复制代码
实现。
[h2]前端一些打包工具的底层实现[/h2]目前一些比较火的
  1. 前端打包构建工具
复制代码
,都是通过
  1. node.js
复制代码
编写的,打包和构建的过程肯定是文件频繁操作的过程,离不来
  1. stream
复制代码
,例如现在比较火的
  1. gulp
复制代码
,有兴趣的小伙伴可以去看一下源码。
[h1]stream的种类 [/h1]
    1. Readable Stream
    复制代码
    可读数据流
    1. Writeable Stream
    复制代码
    可写数据流
    1. Duplex Stream
    复制代码
    双向数据流,可以同时读和写
    1. Transform Stream
    复制代码
    转换数据流,可读可写,同时可以转换(处理)数据(不常用)
之前的文章都是围绕前两种可读数据流和可写数据流,第四种流不太常用,需要的小伙伴网上搜索一下,接下来对第三种数据流Duplex Stream 说明一下。
  1. Duplex Stream
复制代码
双向的,既可读,又可写。
  1. Duplex streams
复制代码
同时实现了
  1. Readable
复制代码
  1. Writable
复制代码
接口。
  1. Duplex streams
复制代码
的例子包括
    1. tcp sockets
    复制代码
    1. zlib streams
    复制代码
    1. crypto streams
    复制代码
    我在项目中还未使用过双工流,一些Duplex Stream的内容可以参考这篇文章NodeJS Stream 双工流
[h1]stream有什么弊端 [/h1]
    1. rs.pipe(ws)
    复制代码
    的方式来写文件并不是把 rs 的内容
    1. append
    复制代码
    到 ws 后面,而是直接用 rs 的内容覆盖 ws 原有的内容
  • 已结束/关闭的流不能重复使用,必须重新创建数据流
    1. pipe
    复制代码
    方法返回的是目标数据流,如
    1. a.pipe(b)
    复制代码
    返回的是 b,因此监听事件的时候请注意你监听的对象是否正确
  • 如果你要监听多个数据流,同时你又使用了
    1. pipe
    复制代码
    方法来串联数据流的话,你就要写成:代码实例:
  1. data        .on('end', function() {            console.log('data end');        })        .pipe(a)        .on('end', function() {            console.log('a end');        })        .pipe(b)        .on('end', function() {            console.log('b end');        });
复制代码
[h1]stream的常见类库 [/h1]
  • event-stream 用起来有函数式编程的感觉
  • awesome-nodejs#streams也是一个不错的第三方stream库,有兴趣的小伙伴可以github看一下
[h1]总结 [/h1]本篇文章属于进阶路线【Node必知必会系列】,看完了这篇文章是不是对stream有了一定的了解,并且知道了node对于文件处理还是有完美的解决方案的。本文中三次展示了
  1. 水桶管道流转图
复制代码
,总要的事情说三遍希望小伙伴们记住它,除了以上内容小伙伴们会不会有一些思考,比如
  • stream数据流转具体内容是什么呢?二进制还是
    1. string
    复制代码
    类型还是其他类型,该类型为stream带来了什么好处?
  • 水桶管道流转图中的水管,也就是
    1. pipe
    复制代码
    函数什么时候触发的呢?在什么情况下触流转发?底层机制是什么?上面的疑问(由于篇幅过长拆分为两篇)会在我
    1. stream
    复制代码
    的第二篇文章为大家详细讲解




经常被面试官考的JavaScript数据类型知识你真的懂吗?
深入理解Javacript从作用域作用域链开始
【JS必知必会】高阶函数详解与实战
公司要求会使用框架vue,面试题会被问及哪些?

交流学习
大家好,我是koala,公众号「程序员成长指北」作者,这篇文章是【Node必知必会系列】的路径知识讲解。目前在做一个node后端工程师进阶路线,加入我们一起学习吧!
[h2]加入我们[/h2]



在看点这里
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:25
帖子:5
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP