Node Stream 流探究

流(stream)是一种在 Node.js 中处理流式数据的抽象接口。 stream 模块提供了一些基础的 API,用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,发送到 HTTP 请求,和 fs.createReadStream 都可以使用流。

流可以是可读的、可写的、或是可读写的。 所有的流都是 EventEmitter 的实例。

流的类型

Node.js 中有四种基本的流类型:

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

v12.x 源码中你能看到这四种类型就是四个 js 文件

Stream.Readable = require('_stream_readable');
Stream.Writable = require('_stream_writable');
Stream.Duplex = require('_stream_duplex');
Stream.Transform = require('_stream_transform');

四种都是 EventEmitter 的实例,都有 close、error 事件

  • 可读流具有监听数据到来的 data 事件等
  • 可写流则具有监听数据已传给底层系统的 finish 事件等
  • Duplex 和 Transform 都同时实现了 Readable 和 Writable 的事件和接口。

为什么要使用流?

设想你要读取一个 1.8GB 的文件,你写下了如下代码

var fs = require('fs')

fs.readFile('./Downloads/021520_256-paco-1080p.mp4', function(err, data) {
  if (err) {
    console.log(err);
  }
  console.log(data)
})

运行得

$ node test.js
<Buffer 00 00 00 20 66 74 79 70 69 73 6f 6d 00 00 02 00 69 73 6f 6d 69 73 6f 32 61 76 63 31 6d 70 34 31 00 58 95 99 6d 6f 6f 76 00 00 00 6c 6d 76 68 64 00 00 ... 1877454492 more bytes>

但若把上文得 readFile 里得文件换成是 2.3 GB 的,运行将会报错

$ node test.js
RangeError [ERR_FS_FILE_TOO_LARGE]: File size (2326080113) is greater than possible Buffer: 2147483647 bytes
    at FSReqCallback.readFileAfterStat [as oncomplete] (fs.js:260:11) {
  code: 'ERR_FS_FILE_TOO_LARGE'
}

总之 v8 的内存限制就是 2GB,那么想要读大文件,那就得利用 stream 流。

设想一下刚刚的读文件的过程就像是要把一桶水从河边运输到楼上,上面的做法就真是搬一桶水,若水的重量超过你的负重上限,那就搬不动。而流就如同是架了一根水管。

流的使用场景

通常我们在文件以及网络的 I/O 操作中能用到。

const fs = require('fs');

const inputStream = fs.createReadStream('input.txt');
const outputStream = fs.createWriteStream('output.txt');

inputStream.pipe(outputStream);

网络I/O,我们知道 http 模块的 请求对象或者另一端的响应对象是 http.IncomingMessage 的实例,它是 Stream.Readable 的子类。下文的代码简单实现了请求代理转发的功能。

var http = require('http');

http.createServer(function(request, response) {
  // request 是 IncomingMessage 的实例
  var options = {
    host: 'localhost',
    port: 9000,
    path: request.url,
    method: request.method,
  }
  http.request(options, function(res) {
    // 这里的 res 是 IncomingMessage 的实例
    res.pipe(response);
  }).end();
}).listen(8080);

http.createServer(function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('request successfully proxied to port 9000!' + '\n' + JSON.stringify(req.headers, true, 2));
  res.end();
}).listen(9000);

流是 EventEmitter 的实例

v12 steams 源码

const EE = require('events');

function Stream(opts) {
  EE.call(this, opts);
}
ObjectSetPrototypeOf(Stream.prototype, EE.prototype);
ObjectSetPrototypeOf(Stream, EE);

确实继承自 events。

pipe 管道

pipe 作为通道,能很好的控制管道里的流,控制读和写的平衡,不让任一方过度操作。

探究这个,请参见 v12.x stream #L15

Stream.prototype.pipe = function(dest, options) {
  const source = this;

  function ondata(chunk) {
    if (dest.writable && dest.write(chunk) === false && source.pause) {
      source.pause();
    }
  }

  source.on('data', ondata);

  function ondrain() {
    if (source.readable && source.resume) {
      source.resume();
    }
  }

  dest.on('drain', ondrain);

  // ... 省略其他事件代码

  dest.emit('pipe', source);

  // Allow for unix-like usage: A.pipe(B).pipe(C)
  return dest;
};

我来解释一下上文代码中的 dest.write(chunk) === false,这是代表 stream 写入的速度比读取的速度快,导致缓存区 Buffer 已经被填满了,那 write 就会返回 false,那么就会调用 source.pause(); 来暂停写入。

那么 dest.on('drain', ondrain); 的意思就好理解了,它代表缓存区的数据已经被读完了,并且排空了,那么触发一个事件,然后恢复流写入缓冲区的动作 source.resume();

有这样的机制存在,就能够保存读写平衡。

有关于缓冲区 Buffer,参见我另一片文章: Node Buffer 对象的探究与内存分配代码挖掘

参考

Github-Node-v12.x streams 源码

Node Buffer 对象的探究与内存分配代码挖掘

认识node核心模块--从Buffer、Stream到fs

posted @ 2020-06-06 15:17  Ever-Lose  阅读(755)  评论(0编辑  收藏  举报