最近加入公司组织的Node学习小组,每个人认领不同的知识点,并和组内同学分享。很喜爱这样的学习模式,除了能够零碎学习外,还能倒逼本人输入,播种颇多,把本人筹备的笔记分享进去。
- 简介
Buffer
- 简介
- 编码
- 内存分配机制
- API概览
stream
- 简介
- 可读流
- 可写流
- 双工流
- 实现与应用
简介
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,也能够指定其余字符编码格局。
注意事项:
- Buffer => utf8:如遇到非UTF-8数据会转换为 �。
- Buffer => utf16le:每个字符会应用2或4个字节进行编码。
- Buffer => latin1:指定了Unicode编码范畴,超出会截断并映射为范畴内的字符串。
Tip:buffer不反对的编码类型,gbk、gb2312等能够借助js工具包iconv-lite实现。
--《深入浅出Node.js》
内存分配机制
因为 Buffer 须要解决的是大量的二进制数据,如果用一点就向零碎去申请,则会造成频繁的向零碎申请内存调用,所以 Buffer 所占用的内存不是由 V8 调配,而是在 Node.js 的 C++ 层面实现申请,在 JavaScript 中进行内存调配。这部分内存称之为堆外内存。
Node.js 采纳了 slab 事后申请、预先分配机制。
- new Buffer1 => 创立slab对象1 => new Buffer2 => 判断slab对象1残余空间是否够用。
- 开释Buffer1、Buffer2对象时,仍然保留slab1对象。
- 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)// 指定填充 1Buffer.alloc(10, 1)// 未初始化的缓冲区 比alloc更快,有可能蕴含旧数据Buffer.allocUnsafe(10)//from创立缓冲区Buffer.from([1,2,3])Buffer.from('test')Buffer.from('test','test2')//相似数据组 能够用 for..ofconst 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,1234const 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 endrr.on('data', (chunk) => { file.write(chunk)});rr.on('end', () => { file.end()});// 3. readable readrr.on('readable', () => { const chunk = rr.read() if(null !== chunk){ file.write(chunk) }else{ file.end() } // 完结时 read()返回null});
可写流
例子:
const Writable = require('stream').Writableconst 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').Duplexvar 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, 1duplex.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('管道传送胜利'); } });// 输入 // 管道传送胜利
实现与应用
实现
如果实现一个新的流,应继承了四个根本流类之一(
stream.Writeable
、stream.Readable
、stream.Duplex
或stream.Transform
),并确保调用了相应的父类构造函数:// 1. 继承const { Readable } = require('stream');class Counter extends Readable { constructor(opt) { // do some thing } _read() { // do some thing }}
新的流类必须实现一个或多个特定的办法,具体取决于要创立的流的类型,如下图所示:
用例 类 须要实现的办法 只读 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}
回顾
- Buffer与stream的类比。
- Buffer为数据缓冲区,Buffer类次要解决二进制。
- Buffer比String更是适宜传输。
- slab分配机制:重复使用。
- Buffer的API概览。
- stream是I/O数据流的形象,有方向、状态、缓冲大小。
- 3种流:可读、可写、可读可写(双工)。
- 双工流中Duplex与Transform区别:读写是否独立。
- stream中Readable、Writable、Duplex、Transform、pipeline的应用。
- 通过继承实现不同类型的流
- 自定义类、构造函数、实例重写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极简入门教程 - 双工流》