关于node.js:理解Node中的Buffer与stream

56次阅读

共计 10026 个字符,预计需要花费 26 分钟才能阅读完成。

最近加入公司组织的 Node 学习小组,每个人认领不同的知识点,并和组内同学分享。很喜爱这样的学习模式,除了能够零碎学习外,还能倒逼本人输入,播种颇多,把本人筹备的笔记分享进去。

  1. 简介
  2. Buffer

    1. 简介
    2. 编码
    3. 内存分配机制
    4. API 概览
  3. stream

    1. 简介
    2. 可读流
    3. 可写流
    4. 双工流
    5. 实现与应用

简介


Buffer 是数据以二进制模式长期寄存在内存中的物理映射,stream 为搬运数据的传送带和加工器,有方向、状态、缓冲大小。

比方咱们实现一个将图片和音频读取到内存而后加工为的视频程序,相似于将原料运输到工厂而后加工为月饼的流程。

Buffer

简介

缓冲区

数据的挪动是为了解决或读取它,如果 数据达到的速度 比过程耗费的速度快,那么多数 早达到的数据会处于期待区等待被解决
《Node.js 中的缓冲区(Buffer)到底是什么?》

咱们读一个秘钥文件进入内存,必定是等整个文件读入内存后再解决,要提前划分寄存的空间。
就像摆渡车一样,坐满了 20 位才发车,乘客有早到有晚到,必须有一个中央等待,这就是缓冲区。

Buffer 是数据以二进制模式长期寄存在内存中的物理映射。

晚期 js 没有读取操作二进制的机制,js 最后设计是为了操作 html。

Node 晚期为了解决图像、视频等文件,将字节编码为字符串来解决二进制数据,速度慢
ECMAScript 2015 公布 TypedArray, 更高效的拜访和解决二进制,用于操作网络协议、数据库、图片和文件 I/O 等一些须要大量二进制数据的场景。

Buffer 对象用于示意固定长度的字节序列。
Buffer 类是 JavaScript 的 Uint8Array 类的子类,且继承时带上了涵盖额定用例的办法。只有反对 Buffer 的中央,Node.js API 都能够承受一般的 Uint8Array
— 官网文档

因为历史起因,晚期的 JavaScript 语言没有用于读取或操作二进制数据流的机制。因为 JavaScript 最后被设计用于解决 HTML 文档,而文档次要由字符串组成。
—《Node.js 企业级利用开发实际》

总结起来一句话 Node.js 能够 用来解决二进制流数据或者与之进行交互
—《Node.js 中的缓冲区(Buffer)到底是什么?》

编解码

将原始字符串与指标字符串进行互转。

编码:将音讯转换为适宜 传输的字节流
解码:将传输的字节流转换为 > 程序可用的音讯格局 –《Node.js 企业级利用开发实战》>

Buffer 与 String 传输比照

const http = require('http');
let s = '';
for (let i=0; i<1024*10; i++) {s+='a'}

const str = s;
const bufStr = Buffer.from(s);
const server = http.createServer((req, res) => {console.log(req.url);

    if (req.url === '/buffer') {res.end(bufStr);
    } else if (req.url === '/string') {res.end(str);
    }
});

server.listen(3000);
# -c 200 并发数  -t 期待响应最大工夫 秒
$ ab -c 200 -t 60 http://localhost:3000/buffer
$ ab -c 200 -t 60 http://localhost:3000/string


雷同的测试参数,Buffer 实现申请 13998 次,string 实现申请 9237 次,相差 4761 次,Buffer 比字符串的的传输更快

反对格局

Buffer 和字符串之间转换时,默认应用 UTF-8,也能够指定其余字符编码格局。

注意事项:

  1. Buffer => utf8:如遇到非 UTF- 8 数据会转换为 �。
  2. Buffer => utf16le:每个字符会应用 2 或 4 个字节进行编码。
  3. Buffer => latin1:指定了 Unicode 编码范畴,超出会截断并映射为范畴内的字符串。

Tip:buffer 不反对的编码类型,gbk、gb2312 等能够借助 js 工具包 iconv-lite 实现。
–《深入浅出 Node.js》

内存分配机制

因为 Buffer 须要解决的是大量的 二进制数据,如果用一点就向零碎去申请,则 会造成频繁的向零碎申请内存调用 ,所以 Buffer 所占用的内存不是由 V8 调配,而是在 Node.js 的 C++ 层面实现申请,在 JavaScript 中进行内存调配。这部分内存称之为 堆外内存

Node.js 采纳了 slab 事后申请、预先分配机制

  1. new Buffer1 => 创立 slab 对象 1 => new Buffer2 => 判断 slab 对象 1 残余空间是否够用。
  2. 开释 Buffer1、Buffer2 对象时,仍然保留 slab1 对象。
  3. new Buffer3 => 将 slab1 对象空间划分给 Buffer3。

slab 对象的三种状态:

小对象创立

Node 认为 8kb 辨别大对象与小对象。当创立的小对象时,调配一个 slab 对象。
再创立一个小对象时,会判断以后的 slab 对象残余空间是足够,如果 够用则应用残余空间,如果不够用则调配新的 slab 空间

const Buffer1 = new Buffer(1024)


const Buffer2 = new Buffer(4000)

slab 调配

slab 是 Linux 操作系统的一种内存分配机制。其工作是 针对一些常常调配并开释的对象,这些对象的大小个别比拟小
如果间接采纳搭档零碎来进行调配和开释,不仅会造成 大量的内存碎片,而且处理速度也太慢

而 slab 分配器是基于对象进行治理的,雷同类型的对象归为一类,每当要申请这样一个对象,slab 分配器就从一个 slab 列表中调配一个这样大小的单元进来 ,而当 要开释时,将其从新保留在该列表中 ,而不是间接返回给搭档零碎, 从而防止这些内碎片

slab 分配器并 不抛弃已调配的对象 ,而是开释并把它们保留在内存中。 当当前又要申请新的对象时,就能够从内存间接获取而不必反复初始化
– 百度百科 slab

文言:一些 小对象常常须要高频次调配、开释 ,导致了 内存碎片和处理速度慢 ,slab 机制是: 不抛弃开释的 slab 对象,将旧 slab 对象间接调配给新 buffer(旧 slab 对象可能蕴含旧数据),以此进步性能

老版本 new Buffer、与新版本 Buffer.allocUnsafe 运行更快,然而内存未初始化,可能导致敏感数据泄露:

手动填充解决:

应用 –zero-fill-buffers 命令行选项解决:

Buffer.alloc 较慢,但更牢靠:

API 概览

简略应用:

// 指定长度初始化
Buffer.alloc(10)
// 指定填充 1
Buffer.alloc(10, 1)

// 未初始化的缓冲区 比 alloc 更快,有可能蕴含旧数据
Buffer.allocUnsafe(10)

//from 创立缓冲区
Buffer.from([1,2,3])
Buffer.from('test')
Buffer.from('test','test2')

// 相似数据组 能够用 for..of
const buf = Buffer.from([1,2,3])
for(const item of buf){console.log(item)
}

// 输入
// 1
// 2
// 3

Node 6~8 版本应用 new Buffer 创立:

// 创立实例
const buf1 = new Buffer()
const buf2 = new Buffer(10)
// 手动笼罩
buf1.fill(0)

slice/concat/compare:

// 1. 切分
const buf = new Buffer.from('buffer')
console.log(buf.slice(0, 4).toString())
// buff


// 2. 连贯
const buf = new Buffer.from('buffer')
const buf1 = new Buffer.from('11111')
const buf2 = new Buffer.from('22222')

const concatBuf = Buffer.concat([buf, buf1, buf2], buf.length + buf1.length + buf2.length)
console.log(concatBuf.toString())
// buffer1111122222


// 3. 比拟
const buf1 = new Buffer.from('1234')
const buf2 = new Buffer.from('0123')
const arr = [buf1, buf2]
arr.sort(Buffer.compare)
console.log(arr.toString())
// 0123,1234

const buf3 = new Buffer.from('4567')
console.log(buf1.compare(buf1))
console.log(buf1.compare(buf2))
console.log(buf1.compare(buf3))
// 0 雷同
// 1 之前
// -1 之后

stream

简介

流(stream)是 Node.js 中解决流式数据的形象接口。stream 模块用于构建实现了流接口的对象。
Node.js 提供了多种流对象。例如,HTTP 服务器的申请和 process.stdout 都是流的实例。
流能够是可读的、可写的、或者可读可写的
— 官网文档

什么是 Stream?
流,英文 Stream 是对输入输出设施的形象,这里的设施能够是文件、网络、内存等。
流是有方向性的,当程序从某个数据源读入数据,会开启一个输出流,这里的数据源能够是文件或者网络等,例如咱们从 a.txt 文件读入数据。相同的当咱们的程序须要写出数据到指定数据源(文件、网络等)时,则开启一个输入流。当有一些大文件操作时,咱们就须要 Stream 像管道一样,一点一点的将数据流出。
–《Node.js 中的缓冲区(Buffer)到底是什么?》

流是输入输出设施的形象,数据从设施流入内存为可读流,从内存流入设施为可写,就向水流管道一样,有方向,也有状态(流动、暂停)。

stream 模块次要用于创立新类型的流实例。对于以生产流对象为主的开发者,极少须要间接应用 stream 模块

stream 有 4 种类型,所有流都是 EventEmitter 对象

  • 可读流:Writable
  • 可写流:Readabale
  • 双工流(可读可写):Duplex
  • 转换流:Transform

简略用法:

const {Writable} = require('stream');
const fs = require('fs');
// 可读流实例
const rr = fs.createReadStream('foo.txt');
// 可写流实例
const myWritable = new Writable({write(chunk, encoding, callback) {// ...}
});

// EventEmitter 用法
myWritable.on('pipe',function(){// do some thing})

myWritable.on('finish',function(){// do some thing})

// 可读流推送到可写流
myWritable.pipe(rr)

对象模式

Node.js 创立的流都是运作在字符串和 Buffer(或 Uint8Array)上。当然,流的实现也能够应用其它类型的 JavaScript 值(除了 null)。这些流会以“对象模式”进行操作。
当创立流时,能够应用 objectMode 选项把流实例切换到对象模式。将已存在的流切换到对象模式是不平安的。
— Node.js v14.16.0

缓冲

highWaterMark 选项 指定了可缓冲数据大小,即字节总数,对象模式的流为对象总数。

可读流 缓冲达到 highWaterMark 指定的值时,会进行从底层资源读取数据 ,直到数据被生产。
可写流 缓冲达到 highWaterMark 值时 writable.write()返回 false。

stream.pipe()会限度缓冲,防止读写不统一导致内存解体

可读流

2 种模式

  • 暂停 Paused 模式
  • 流动 Flowing 模式

这两种模式是基于 readable.readableFlowing 的 3 种外部状态的一种简化形象。

  • readable.readableFlowing = null 没有提供生产流数据的机制,此时指定 data、指定 pipe、执行 resume 会使值变为 true
  • readable.readableFlowing = true 调用 pause、unpipe 会使值变为 false
  • readable.readableFlowing = false

暂停模式对应 null 和 false。

抉择一种接口格调

Node 提供了多种办法来生产流数据。开发者通常应该抉择其中一种办法来生产数据,不要在单个流应用多种办法来生产数据。混合应用 on('data')on('readable')pipe() 或异步迭代器,会导致不明确的行为。

const fs = require('fs');
const rr = fs.createReadStream('api.xmind');
const file = fs.createWriteStream('api.xmind.file');

// 1. 可读流绑定可写流
rr.pipe(file)
rr.unpipe(file)


// 2. data end
rr.on('data', (chunk) => {file.write(chunk)
});
rr.on('end', () => {file.end()
});


// 3. readable read
rr.on('readable', () => {const chunk = rr.read()
  if(null !== chunk){file.write(chunk)
  }else{file.end()
  }
  // 完结时 read()返回 null
});

可写流

例子:

const Writable = require('stream').Writable

const writable = Writable()
// 实现 `_write` 办法
// 这是将数据写入底层的逻辑
writable._write = function (data, enc, next) {
  // 将流中的数据写入底层
  process.stdout.write(data.toString().toUpperCase())
  // 写入实现时,调用 `next()` 办法告诉流传入下一个数据
  process.nextTick(next)
}

// 所有数据均已写入底层
writable.on('finish', () => process.stdout.write('DONE'))

// 将一个数据写入流中
writable.write('a' + '\n')
writable.write('b' + '\n')
writable.write('c' + '\n')

// 再无数据写入流时,须要调用 `end` 办法
writable.end()

// 输入
// A
// B
// C
// DONE%

cork/uncork 办法

writable.cork() 办法强制把所有写入的数据都缓冲到内存中。当调用 stream.uncork() 或 stream.end() 办法时,缓冲的数据才会被输入。

stream.cork();
stream.write('一些');
stream.write('数据');
process.nextTick(() => stream.uncork());

如果一个流上屡次调用 writable.cork(),则必须调用同样次数的 writable.uncork() 能力输入缓冲的数据。

stream.cork();
stream.write('一些');
stream.cork();
stream.write('数据');
process.nextTick(() => {stream.uncork();
  // 数据不会被输入,直到第二次调用 uncork()。stream.uncork();});

双工流

双工流(Duplex)是同时实现了可读、可写的流,包含 TCP socket、zlib、crypto。
转换流(Transform)是双工流的一种,例 zlib、crypto。

区别:Duplex 尽管同时具备可读流和可写流,但 两者是独立的 ;Transform 的可读流的数据会通过肯定的处理过程 主动进入可写流

例子,实现_read、_write 办法,将写入数据转为 1、2:

var Duplex = require('stream').Duplex
var duplex = Duplex()

// 可读端底层读取逻辑
duplex._read = function () {
  this._readNum = this._readNum || 0
  if (this._readNum > 1) {this.push(null)
  } else {this.push('' + (this._readNum++))
  }
}

// 可写端底层写逻辑
duplex._write = function (buf, enc, next) {
  // a, b
  process.stdout.write('_write' + buf.toString() + '\n')
  next()}

// 0, 1
duplex.on('data', data => console.log('ondata', data.toString()))
duplex.write('a')
duplex.write('b')
duplex.end()

// 输入
// _write a
// _write b
// ondata 0
// ondata 1

转换流是一种非凡双工流,对输出计算后再输出,如加解密、zlib 流、crypto 流。输出、输出的数据流大小、数据块数量不肯定统一。如果可读端的数据没有被生产,可写流的数据可能会被暂停。

例子,通过 transform 办法实现大小写转换:

const {Transform} = require('stream');

const upperCaseTr = new Transform({transform(chunk, encoding, callback) {this.push(chunk.toString().toUpperCase());
    callback();}
});

upperCaseTr.on('data', data => process.stdout.write(data))
upperCaseTr.write('hello,')
upperCaseTr.write('world!')
upperCaseTr.end()

// 输入 HELLO, WORLD!%  

内置转换流

// 应用 pipe 创立.gz 压缩文件
const fs = require('fs');
const zlib = require('zlib');
const fileName = 'api.xmind'
fs.createReadStream(fileName)
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream(fileName + '.gz'));
// 应用 pipe + transform + on 实现进度打印
const fs = require('fs');
const zlib = require('zlib');
const fileName = 'api.xmind'
const {Transform} = require('stream');

const reportProgress = new Transform({transform(chunk, encoding, callback) {process.stdout.write('.');
    callback(null, chunk);
  }
});

fs.createReadStream(fileName)
  .pipe(zlib.createGzip())
  .pipe(reportProgress)
  .pipe(fs.createWriteStream(fileName + '.zz'))
  .on('finish', () => console.log('Done'));

// 输入
// ........Done
// 应用 pipeline 办法 实现管道
const {pipeline} = require('stream');
const fs = require('fs');
const zlib = require('zlib');
const fileName = 'api'
// 应用 pipeline API 轻松地将一系列的流通过管道一起传送,并在管道齐全地实现时取得告诉。// 应用 pipeline 能够无效地压缩一个可能很大的 tar 文件:pipeline(fs.createReadStream(fileName + '.xmind'),
  zlib.createGzip(),
  fs.createWriteStream(fileName + '.tar.gz'),
  (err) => {if (err) {console.error('管道传送失败', err);
    } else {console.log('管道传送胜利');
    }
  }
);

// 输入 
// 管道传送胜利

实现与应用

实现

  1. 如果实现一个新的流,应继承了四个根本流类之一(stream.Writeablestream.Readablestream.Duplex 或 stream.Transform),并确保调用了相应的父类构造函数:

    // 1. 继承
    const {Readable} = require('stream');
    class Counter extends Readable {constructor(opt) {// do some thing}
      _read() {// do some thing}
    }
  2. 新的流类必须实现一个或多个特定的办法,具体取决于要创立的流的类型,如下图所示:

    用例 须要实现的办法
    只读 Readable _read()
    只写 Writable _write()_writev()_final()
    可读可写 Duplex _read()_write()_writev()_final()
    对写入的数据进行操作,而后读取后果 Transform _transform()_flush()_final()

防止重写诸如 write()end()cork()uncork()read() 和 destroy() 之类的公共办法,或通过 .emit() 触发诸如 'error''data''end''finish' 和 'close' 之类的外部事件。这样做会毁坏以后和将来的流的不变量,从而导致与其余流、流的实用工具、以及用户冀望的行为和 / 或兼容性问题。

应用

// 1. 应用自定义构造函数
const {Readable} = require('stream');
class Counter extends Readable {constructor(opt) {// do some thing}
  _read() {// do some thing}
}
const myReadable = new Counter()

// 2. 应用原生构造函数
const {Readable} = require('stream');
const myReadable = new Readable({read(size) {// do some thing}
});

// 3. 重写实例办法
const {Readable} = require('stream');
const myReadable = Readable()
myReadable._write = function (buf, enc, next) {// do some thing}

回顾

  1. Buffer 与 stream 的类比。
  2. Buffer 为数据缓冲区,Buffer 类次要解决二进制。
  3. Buffer 比 String 更是适宜传输。
  4. slab 分配机制:重复使用。
  5. Buffer 的 API 概览。
  6. stream 是 I / O 数据流的形象,有方向、状态、缓冲大小。
  7. 3 种流:可读、可写、可读可写(双工)。
  8. 双工流中 Duplex 与 Transform 区别:读写是否独立。
  9. stream 中 Readable、Writable、Duplex、Transform、pipeline 的应用。
  10. 通过继承实现不同类型的流
  11. 自定义类、构造函数、实例重写 3 种应用形式

七拼八凑的知识点,如有问题 恳请斧正,以防误导别人

参考资料:

  • Node.js 中文网 v14.16.1 文档
  • 《深入浅出 Node.js》
  • 《Node.js 企业级利用开发实际》
  • 《Node.js 中的缓冲区(Buffer)到底是什么?》
  • 《译 - 你所须要晓得的对于 Node.js Streams 的所有》
  • 《美团技术 Node.js Stream – 根底篇》
  • 《美团技术 Node.js Stream – 进阶篇》
  • 《美团技术 Node.js Stream – 实战篇》
  • 《Node 极简入门教程 – 双工流》

正文完
 0