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

但若把上文得 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

你可能感兴趣的:(Node Stream 流探究)