内置队列或缓存:可以理解为node根据流输入的数据,用一个链表数据结构建立的缓存,读取、写出的内容都需要经过缓存。(参考专业说法:内置队列MDN)
highWaterMark水平线或阈值:内置队列需要设立上限,否则会突破node的内存限制大小,从而成为一种攻击手段
https://nodejs.org/docs/latest-v18.x/api/stream.html#writablewritechunk-encoding-callback
攻击手段:A给node服务发送world,让它帮A转换html文件,A扮演着发送流,也扮演着接受流;此时A只发送,但是决绝接受,一旦这个文件超过node内存限制,也就意味着这个node服务将会内存泄露,从而宕机,hack成功!
node
能使用的内存大小?(为什么不说web,虽然没有刻意去了解web,V8内存管理和node一致;但其他GUI渲染内存加上去绝对和node内存不一样)这个相信大家都知道,新生代(32 位系统分配 16M 的内存空间,64 位系统翻倍 32M),老生代(64位系统下约为1.4GB,32位系统下约为0.7GB),也就是我们能用V8进行内存管理js堆内存只有1.4G;所以如果有大量缓存数据,最好的办法是移除node之外,使用
redis
处理;如果有1个G的文件需要给前端下载怎么办呢?流式永远是最好的解决方案,对于node,不,对于所有后台开发来说,节省内存最好的办法就是流式,流的作用就是读多少传多少,读1M数据传1M数据给前端,大大减轻了V8内存的负担
该方案,确实是一个解决方案;但是V8的各种垃圾回收算法同时也会降低效率(虽然底层会并发清理,但大内存空间消耗的时间一定是成正比的),本文不会对V8垃圾回收机制展开讲解,感兴趣的同学可以搜相关的只是:
新生代的Scavenge算法(from-to通过空间换时间)
,老生代的Mark-Sweep(标记扫除)
、Mark-Compact(标记压缩)时间换空间做法
,V8确认一个数据需要被垃圾回收而又不影响其他堆数据的使用三色标记法(增量标记、强三原色、写屏障这些来保证一个数据被回收而不影响应用正常运行)
const { ReadableStream } = require("node:stream/web");
const { setInterval, setTimeout: timer } = require("node:timers/promises");
const { performance } = require("node:perf_hooks");
const { Buffer } = require("node:buffer");
const readable = new ReadableStream({
// 开始事件
async start(controller) {
console.log("start.");
},
// 当内置队列未满时,一直读取,如果为异步则等待异步完成后再次调用
async pull(controller) {
await timer(100); // 500ms 读取一次
const val = performance.now();
controller.enqueue(val);
console.log("队列剩余容量", controller.desiredSize);
},
// 取消事件 可以通过reader.cancel()方法取消流pull读取事件
cancel(reason) {
console.log(reason);
},
},
{
highWaterMark: 5, // 水平线
// 根据返回的number大小,水平线 - size返回的大小 = 当前剩余容量(controller.desiredSize)
size(chunk) {
return 1;
},
});
(async () => {
// 消费5次
const reader = readable.getReader(); // 默认的reader实例,允许js值(如:对象...)
for (let index = 1; index <= 5; index++) {
console.log(await reader.read());
}
// 2s后消费3次
setTimeout(async () => {
console.log(await reader.read());
console.log(await reader.read());
console.log(await reader.read());
}, 2000);
})();
/*
// 开始事件
start.
// 这块生产消费同时在进行所以,内置队列大小没变
队列剩余容量 5
{ value: 201.76770899817348, done: false }
队列剩余容量 5
{ value: 304.59966699779034, done: false }
队列剩余容量 5
{ value: 406.3125419989228, done: false }
队列剩余容量 5
{ value: 508.1209169998765, done: false }
队列剩余容量 5
{ value: 611.398583997041, done: false }
// 一直读取中
队列剩余容量 4
队列剩余容量 3
队列剩余容量 2
队列剩余容量 1
队列剩余容量 0
// 读取完毕到达阈值(内置队列容量为0)
// 定时器2s,开始消费
{ value: 655.7073750011623, done: false }
{ value: 757.9737910032272, done: false }
{ value: 859.5705410018563, done: false }
// 消费了3个自然要读取3个
队列剩余容量 2
队列剩余容量 1
队列剩余容量 0
*/
const { ReadableStream, WritableStream } = require("node:stream/web");
const { setInterval, setTimeout: timer } = require("node:timers/promises");
const { performance } = require("node:perf_hooks");
const { Buffer } = require("node:buffer");
// 可读流
const readable = new ReadableStream(
{
async pull(controller) {
await timer(500); // 500ms 读取一次
const val = performance.now();
controller.enqueue(val);
console.log("队列剩余容量", controller.desiredSize);
},
},
{
highWaterMark: 5,
size(chunk) {
return 1;
},
},
);
// 可写流
const writeable = new WritableStream({
write(chunk) {
console.log("写入流接收到的数据", chunk);
},
});
(async () => {
const writer = writeable.getWriter();
// 不使用Reader读取器消费,可以使用for await来进行消费,将读取到的数据写入到写入流里
for await (const value of readable) {
writer.write(value);
}
})();
/*
队列剩余容量 5
写入流接收到的数据 539.5047909989953
队列剩余容量 5
写入流接收到的数据 1051.5886659994721
队列剩余容量 5
写入流接收到的数据 1553.0724160000682
队列剩余容量 5
写入流接收到的数据 2055.640707999468
队列剩余容量 5
写入流接收到的数据 2558.0102079994977
... // 一边生产一边消费
*/
MDNfor await of异步迭代器:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for-await…of
ReadableStream支持异步迭代器:https://nodejs.org/docs/latest-v18.x/api/webstreams.html#async-iteration
const { ReadableStream, WritableStream } = require("node:stream/web");
const { setTimeout: timer } = require("node:timers/promises");
const { performance } = require("node:perf_hooks");
// 可读流
const readable = new ReadableStream(
{
async pull(controller) {
await timer(100); // 100ms 读取一次
const val = performance.now();
controller.enqueue(val);
console.log("队列剩余容量", controller.desiredSize);
},
},
{
highWaterMark: 5,
size(chunk) {
return 1;
},
},
);
// 可写流 1s钟读取一次
const writeable = new WritableStream({
async write(chunk, controller) {
await timer(1000);
console.log("写入流接收到的数据", chunk);
},
});
(async () => {
// 效果:当reader读完满内置队列之后,writer只有写入完成后,reader才会继续读,强制当水平线
const writer = writeable.getWriter();
for await (const value of readable) {
await writer.write(value);
}
})();
/*
// 生产者读取的很快
队列剩余容量 5
队列剩余容量 4
队列剩余容量 3
队列剩余容量 2
队列剩余容量 1
队列剩余容量 0
// 生产者读取到达阈值,停止读取
写入流接收到的数据 141.93745799735188 // 消费成功
队列剩余容量 0 // 消费一个,读一个
写入流接收到的数据 249.89816699922085
队列剩余容量 0
写入流接收到的数据 351.6370829977095
队列剩余容量 0
写入流接收到的数据 453.2700829990208
队列剩余容量 0
...
*/
const { TransformStream } = require("node:stream/web");
const transform = new TransformStream(
{
// 可写流写入出发转换过程
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
// 写入流关闭执行
flush(controller) {
console.log("写入流关闭!");
},
},
// 可写流阈值配置
{
highWaterMark: 5,
size() {
return 1;
},
},
// 可读流阈值配置
{
highWaterMark: 5,
size() {
return 1;
},
},
);
(async () => {
const writer = transform.writable.getWriter();
const reader = transform.readable.getReader();
await writer.write("abc");
const value = await reader.read();
console.log(value);
writer.close();
})();
/*
{ value: 'ABC', done: false }
写入流关闭!
*/
相较于传统的stream,web stream内置了背压机制
pipe()
管道,也无需刻意注意背压机制,web stream底层已经帮我们处理,即highwatermark为强制背压的水平线解耦
了MDN流API文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Streams_API
Nodejs官方web stream API文档:https://nodejs.org/docs/latest-v16.x/api/webstreams.html#class-readablestream
内置队列MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Streams_API/Concepts#%E5%86%85%E7%BD%AE%E9%98%9F%E5%88%97%E5%92%8C%E9%98%9F%E5%88%97%E7%AD%96%E7%95%A5
Node通过传统流的攻击手段:https://nodejs.org/docs/latest-v18.x/api/stream.html#writablewritechunk-encoding-callback
MDNfor await of异步迭代器:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for-await…of
ReadableStream支持异步迭代器:[https://nodejs.org/docs/latest-v18.x/api/webstreams.html#async-iteration](