/ NodeJS

NodeJS Stream 理解

在unix中,我们可以使用|符号来实现流,我们经常会在Shell中用到管道操作符。在Node中,流模块的基本操作符叫做.pipe()。其中 Stream 涉及了操作系统中经典的生产者-消费者模型

Stream 是 Node 移动数据的方式。Node JS有四种基本的Stream类型:

  • Writable - 可写入数据的流(如 fs.createWriteStream())。
  • Readable - 可读取数据的流(如 fs.createReadStream())。
  • Duplex - 可读又可写的流(如 net.Socket)。
  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(如 zlib.createDeflate())。

对于Stream,理解消费者-生产者模型即可明白原理,如何使用官方文档上也很清晰,在大部分业务场景中,我们使用.pipe()方法来实现流的输入和输出即可,并由内部逻辑帮我们来处理读取和写入速度。

Stream 流使用对比

这里准备一个大小为29.5MB的data.txt文件,进行性能测试。
我们可能会写出下面这种代码。在每次请求时,我们都会把整个data.txt文件读入到内存中,然后再把结果返回给客户端。

    const http = require('http');
    const fs = require('fs');

    const server = http.createServer((req, res) => {
        fs.readFile(__dirname + '/data.txt', (err, data) => {
            res.end(data);
        });
    });
    server.listen(8011);

使用ab -c 200 -t 100 http://127.0.0.1:8011/命令来进行性能测试,发起200个并发客户端。
snipaste_2018-08-20_02-49-45
上面代码由于每次请求时,会把整个data.txt文件读入到内存中,然后再把结果返回给客户端。并发过大,内存直接溢出,导致Node进程挂掉。

不过(req,res)参数都是流对象,可以使用一种更好的方法来实现上面的需求:

    const http = require('http');
    const fs = require('fs');

    const server = http.createServer((req, res) => {
        const stream = fs.createReadStream(__dirname + '/data.txt');
        stream.pipe(res);
    });
    server.listen(8011);

同样使用ab -c 200 -t 100 http://127.0.0.1:8001/测试,发起200个并发客户端。
stream-pipe-stream-1
在这里,.pipe()方法会自动帮助我们处理读取速度,且将data.txt文件中每一小段数据将不断的发送到客户端,虽然QPS只有17.39,但是最大的保证了服务的可用性。

http服务中还有一个常见的需求,就是对数据进行gzip压缩,这里可以方便的使用流模块中的.pipe来处理。

    const http = require('http');
    const fs = require('fs');
    const zlib = require('zlib');
    const gzip = zlib.createGzip();

    const server = http.createServer((req, res) => {
        const stream = fs.createReadStream(__dirname + '/data.txt');
        stream.pipe(gzip).pipe(res);
    });
    server.listen(8011);

首先读取到文件流,然后进过zlib流压缩,再将数据传输到TCP流中完成。
Stream模块让业务代码像连水管,极大的提高了我们的开发效率,当然性能也是杠杠的,所以以后能使用流的地方尽可能的使用。