Node.js的优势在于对IO密集任务的处理

一、Buffer(缓冲区)

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。而在对TCP流或者文件流进行处理的时候,必须使用二进制数据,因此在Node中定义了一个Buffer类,用于创建一个专门存放二进制数据的缓存区。
Buffer 类的实例非常类似整数数组, 但其大小是固定不变的, 并且其内存在 V8 堆栈外分配原始内存空间. Buffer 类的实例创建之后, 其所占用的内存大小就不能再进行调整。Buffer类在全局作用域中,因此无需去require导入

  • Buffer.from() 根据已有数据生成一个 Buffer 对象
  • Buffer.alloc() 创建一个初始化后的 Buffer 对象
  • Buffer.allocUnsafe() 创建一个未初始化的 Buffer 对象

Node的Buffer在ES6增加TypedArray 类型后,修改了原来Buffer实现,选择基于TypedArray中的Unit8Array来实现,提升了性能

  1. const arr = new Uint16Array(3)
  2. arr[0] = 5000;
  3. arr[1] = 4000;
  4. arr[2] = 3000;
  5. const buf1 = Buffer.from(arr) //拷贝 buffer
  6. const buf2 = Buffer.from(arr.buffer) //与该数组共享了内存
  7. console.log(buf1); // 输出: <Buffer 88 a0 b8>
  8. console.log(buf2); // 输出: <Buffer 88 13 a0 of b8 0b>
  9. arr[1] = 6000;
  10. console.log(buf1); // 输出: <Buffer 88 a0 b8>
  11. console.log(buf2); // 输出: <Buffer 88 13 70 17 b8 0b>


二、String Decoder(字符串解码器)

字符串解码器 (string_decoder) 是一个用于将 Buffer 拿来 decode 到 string 的模块, 是作为 Buffer.toString 的一个补充, 它支持多字节 UTF-8 和 UTF-16 字符.

  1. const { StringDecoder } = require('string_decoder');
  2. const decoder = new StringDecoder('utf8');
  3. const cent = Buffer.from([0xC2, 0xA2]);
  4. console.log(decoder.write(cent));
  5. const euro = Buffer.from([0xE2, 0x82, 0xAC]);
  6. console.log(decoder.write(euro));

捕获.PNG

Buffer 实例写入 StringDecoder 实例时,将使用内部缓冲区来确保已解码的字符串不包含任何不完整的多字节字符。 它们保存在缓冲区中,直到下一次调用 stringDecoder.write() 或调用 stringDecoder.end() 为止。

  1. const { StringDecoder } = require('string_decoder');
  2. const decoder = new StringDecoder('utf8');
  3. decoder.write(Buffer.from([0xE2]));
  4. decoder.write(Buffer.from([0x82]));
  5. let result = decoder.end(Buffer.from([0xAC])));
  6. console.log(result); // €

三、Stream(流)

在服务端,Stream是一种很应用很广泛的编程方式,C、Java等语言中也都Stream的运用

应用的场景很简单, 你要拷贝一个 20G 大的文件, 如果你一次性将 20G 的数据读入到内存, 你的内存条可能不够用, 或者严重影响性能. 但是你如果使用一个 1MB 大小的缓存 (buffer) 每次读取 1Mb, 然后写入 1Mb, 那么不论这个文件多大都只会占用 1Mb 的内存.

Stream的类型有以下四种:

  • Readable 只读
  • Writeable 只写
  • Duplex 读写
  • Transform 操作被写入的数据, 然后读出结果

缓冲区

Readable 和 Writable 流都会将数据储存在内部的缓冲区中, 缓冲区可以通过Writable._writalbeState.getBuffer()和Readable._readalbeState.buffer 来访问缓冲区的大小,由构造stream时候的highWaterMark标志指定
可容纳的字节大小

可写流

可写实例上不停地调用 writable.write(chunk) 的时候数据会被写入可写流的缓冲区. 如果当前缓冲区的缓冲的数据量highWateMark低于设定的值, 调用 writable.write() 方法会返回 true (表示数据已经写入缓冲区), 否则当缓冲的数据量达到了阈值, 数据无法写入缓冲区 write 方法会返回 false, 直到 drain 事件触发之后才能继续调用 write 写入。

  1. const { Writable } = require('stream');
  2. const outStream = new Writable({
  3. // chunk 通常是一个 buffer
  4. // callback 是一个当我们处理完数据块之后需要调用的一个函数
  5. write(chunk, encoding, callback) {
  6. console.log(chunk.toString());
  7. callback();
  8. }
  9. });
  10. process.stdin.pipe(outStream);


可读流

可读实例调用stream.push()方法的时候, 数据将会被推入缓冲区. 如果数据没有被消费, 即调用stream.read()方法读取的话, 那么数据会一直留在缓冲队列中. 当缓冲区中的数据到达highWaterMark指定的阈值, 可读流将停止从底层汲取数据, 直到当前缓冲的报备成功消耗为止.

  1. const { Readable } = require('stream');
  2. const inStream = new Readable({});
  3. inStream.push('ABCDEFGHIJKLM');
  4. inStream.push('----');
  5. inStream.push('NOPQRSTUVWXYZ');
  6. inStream.push(null); // No more data
  7. inStream.pipe(process.stdout);


Duplex 与 Transform

Duplex 流和 Transform 流都是同时可读写的, 他们会在内部维持两个缓冲区, 分别对应读取和写入, 这样就可以允许两边同时独立操作, 维持高效的数据流。
比如 net.Socket 是一个 Duplex 流, Readable 端允许从 socket 获取、消耗数据, Writable 端允许向 socket 写入数据. 数据写入的速度很有可能与消耗的速度有差距, 所以两端可以独立操作和缓冲是很重要的.

pipe

pipe可以理解为一种管道,最主要的目的就是将数据的流动缓冲到一个可接受的水平, 不让不同速度的数据源之间的差异导致内存被占满。

  1. const fs = require('fs');
  2. const zlib = require('zlib');
  3. const file = process.argv[2];
  4. fs.createReadStream(file)
  5. .pipe(zlib.createGzip())
  6. .pipe(fs.createWriteStream(file + '.gz'));

参考链接

https://github.com/ElemeFE/node-interview/blob/master/sections/zh-cn/io.md
https://cnodejs.org/topic/56ba030271204e03637a3870