• 24小时服务热线:400-088-1128

当前位置 南顺网络>> 知识拓展

想学Node.js,必须了解stream

什么是stream

流的英文stream,流(Stream)是一个抽象的数据接口,Node.js中很多对象都实现了流,流是EventEmitter对象的一个实例,总之它是会冒数据(以 Buffer 为单位),或者能够吸收数据的东西,它的本质就是让数据流动起来。

注意:stream不是node.js独有的概念,而是一个操作系统最基本的操作方式,只不过node.js有API支持这种操作方式。linux命令的|就是stream。
读取大文件data的例子
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,在响应大量用户并发请求的时候,程序可能会消耗大量的内存,这样可能造成用户连接缓慢的问题。而且并发请求过大的话,服务器内存开销也会很大。这时候我们来看一下用stream实现

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就可以不需要把文件全部读取了再返回,而是一边读取一边返回,数据通过管道流动给客户端,真的减轻了服务器的压力。

看了两个例子我想小伙伴们应该知道为什么要使用stream了吧!因为一次性读取,操作大文件,内存和网络是吃不消的,因此要让数据流动起来,一点点的进行操作。

stream应用场景

stream的应用场景主要就是处理IO操作,而http请求和文件操作都属于IO操作。这里再提一下stream的本质——由于一次性IO操作过大,硬件开销太多,影响软件运行效率,因此将IO分批分段进行操作,让数据像水管一样流动起来,直到流动完成,也就是操作完成。下面对几个常用的应用场景分别进行介绍

介绍一个压力测试的小工具
一个对网络请求做压力测试的工具ab,ab 全称 Apache bench ,是 Apache 自带的一个工具,因此使用 ab 必须要安装 Apache 。mac os 系统自带 Apache ,windows用户视自己的情况进行安装。运行ab 之前先启动 Apache ,mac os 启动方式是 sudo apachectl start 。

Apache bench对应参数的详细学习地址,有兴趣的可以看一下 Apache bench对应参数的详细学习地址

介绍这个小工具的目的是对下面几个场景可以进行直观的测试,看出使用stream带来了哪些性能的提升。

get请求中应用stream
这样一个需求:

使用node.js实现一个http请求,读取data.txt文件,创建一个服务,监听8000端口,读取文件后返回给客户端,讲get请求的时候用一个常规文件读取与其做对比,请看下面的例子。

常规使用文件读取返回给客户端response例子 ,文件命名为getTest1.js

// getTest.js
const 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 将上面代码做部分修改,文件命名为getTest2.js

// 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对象,是的没错,对于那张水桶管道流转图,response就是一个dest。

虽然get请求中可以使用stream,但是相比直接file文件读取·res.end(data)有什么好处呢?这时候我们刚才推荐的压力测试小工具就用到了。getTest1和getTest2两段代码,将data.txt内容增加大一些,使用ab工具进行测试,运行命令ab -n 100 -c 100 http://localhost:8000/,其中-n 100表示先后发送100次请求,-c 100表示一次性发送的请求数目为100个。对比结果分析使用stream后,有非常大的性能提升,小伙伴们可以自己实际操作看一下。


post中使用stream
一个通过post请求微信小程序的地址生成二维码的需求。

/*

* 微信生成二维码接口

* 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对象,然后直接.pipe(ws),将post请求的数据流转到这个本地文件中,这种stream的应用在node后端开发过程中还是比较常用的。
在文件操作中使用stream
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('拷贝完成')

})

看了这段代码,发现是不是拷贝好像很简单,创建一个可读数据流readStream,一个可写数据流writeStream,然后直接通过pipe管道把数据流转过去。这种使用stream的拷贝相比存文件的读写实现拷贝,性能要增加很多,所以小伙伴们在遇到文件操作的需求的时候,尽量先评估一下是否需要使用stream实现。

前端一些打包工具的底层实现
目前一些比较火的前端打包构建工具,都是通过node.js编写的,打包和构建的过程肯定是文件频繁操作的过程,离不来stream,例如现在比较火的gulp,有兴趣的小伙伴可以去看一下源码。

stream的种类
Readable Stream 可读数据流
Writeable Stream 可写数据流
Duplex Stream 双向数据流,可以同时读和写
Transform Stream 转换数据流,可读可写,同时可以转换(处理)数据(不常用)
之前的文章都是围绕前两种可读数据流和可写数据流,第四种流不太常用,需要的小伙伴网上搜索一下,接下来对第三种数据流Duplex Stream 说明一下。

Duplex Stream 双向的,既可读,又可写。 Duplex streams同时实现了 Readable和Writable 接口。 Duplex streams的例子包括

tcp sockets
zlib streams
crypto streams 我在项目中还未使用过双工流,一些Duplex Stream的内容可以参考这篇文章NodeJS Stream 双工流

stream有什么弊端
用 rs.pipe(ws) 的方式来写文件并不是把 rs 的内容 append 到 ws 后面,而是直接用 rs 的内容覆盖 ws 原有的内容
已结束/关闭的流不能重复使用,必须重新创建数据流
pipe 方法返回的是目标数据流,如 a.pipe(b) 返回的是 b,因此监听事件的时候请注意你监听的对象是否正确
如果你要监听多个数据流,同时你又使用了 pipe 方法来串联数据流的话,你就要写成

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

来源:掘金
链接:https://juejin.im/post/5d25ce36f265da1ba84ab97a