流(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