从前端转入 Node.js 的童鞋对这一部份内容会比较陌生,由于在前端中一些简单的字符串操做已经知足基本的业务需求,有时可能也会以为 Buffer、Stream 这些会很神秘。回到服务端,若是你不想只作一名普通的 Node.js 开发工程师,你应该深刻去学习一下 Buffer 揭开这一层神秘的面纱,同时也会让你对 Node.js 的理解提高一个水平。html
缓冲(Buffer)与缓存(Cache)的区别?
在引入 TypedArray 以前,JavaScript 语言没有用于读取或操做二进制数据流的机制。 Buffer 类是做为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操做、以及其余上下文中与八位字节流进行交互。这是来自 Node.js 官网的一段描述,比较晦涩难懂,总结起来一句话 Node.js 能够用来处理二进制流数据或者与之进行交互。前端
Buffer 用于读取或操做二进制数据流,作为 Node.js API 的一部分使用时无需 require,用于操做网络协议、数据库、图片和文件 I/O 等一些须要大量二进制数据的场景。Buffer 在建立时大小已经被肯定且是没法调整的,在内存分配这块 Buffer 是由 C++ 层面提供而不是 V8 具体后面会讲解。node
在这里不知道你是否定为这是很简单的?可是上面提到的一些关键词二进制
、流(Stream)
、缓冲区(Buffer)
,这些又都是什么呢?下面尝试作一些简单的介绍。git
谈到二进制咱们大脑可能会浮想到就是 010101 这种代码命令,以下图所示:github
正如上图所示,二进制数据使用 0 和 1 两个数码来表示的数据,为了存储或展现一些数据,计算机须要先将这些数据转换为二进制来表示。例如,我想存储 66 这个数字,计算机会先将数字 66 转化为二进制 01000010 表示,印象中第一次接触这个是在大学期间 C 语言课程中,转换公式以下所示:面试
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
上面用数字举了一个示例,咱们知道数字只是数据类型之一,其它的还有字符串、图像、文件等。例如咱们对一个英文 M 操做,在 JavaScript 里经过 'M'.charCodeAt()
取到对应的 ASCII 码以后(经过以上的步骤)会转为二进制表示。算法
流,英文 Stream 是对输入输出设备的抽象,这里的设备能够是文件、网络、内存等。数据库
流是有方向性的,当程序从某个数据源读入数据,会开启一个输入流,这里的数据源能够是文件或者网络等,例如咱们从 a.txt 文件读入数据。相反的当咱们的程序须要写出数据到指定数据源(文件、网络等)时,则开启一个输出流。当有一些大文件操做时,咱们就须要 Stream 像管道同样,一点一点的将数据流出。api
举个例子缓存
咱们如今有一大罐水须要浇一片菜地,若是咱们将水罐的水一下所有倒入菜地,首先得须要有多么大的力气(这里的力气比如计算机中的硬件性能)才可搬得动。若是,咱们拿来了水管将水一点一点流入咱们的菜地,这个时候不要这么大力气就可完成。
经过上面的讲解进一步的理解了 Stream 是什么?那么 Stream 和 Buffer 之间又是什么关系呢?看如下介绍,关于 Stream 自己也有不少知识点,欢迎关注公众号「Nodejs技术栈」,以后会单独进行介绍。
经过以上 Stream 的讲解,咱们已经看到数据是从一端流向另外一端,那么他们是如何流动的呢?
一般,数据的移动是为了处理或者读取它,并根据它进行决策。伴随着时间的推移,每个过程都会有一个最小或最大数据量。若是数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,若是数据到达的速度比进程消耗的数据慢,那么早先到达的数据须要等待必定量的数据到达以后才能被处理。
这里的等待区就指的缓冲区(Buffer),它是计算机中的一个小物理单位,一般位于计算机的 RAM 中。这些概念可能会很难理解,不要担忧下面经过一个例子进一步说明。
公共汽车站乘车例子
举一个公共汽车站乘车的例子,一般公共汽车会每隔几十分钟一趟,在这个时间到达以前就算乘客已经满了,车辆也不会提早发车,早到的乘客就须要先在车站进行等待。假设到达的乘客过多,后到的一部分则须要在公共汽车站等待下一趟车驶来。
在上面例子中的等待区公共汽车站,对应到咱们的 Node.js 中也就是缓冲区(Buffer),另外乘客到达的速度是咱们不能控制的,咱们能控制的也只有什么时候发车,对应到咱们的程序中就是咱们没法控制数据流到达的时间,能够作的是能决定什么时候发送数据。
了解了 Buffer 的一些概念以后,咱们来看下 Buffer 的一些基本使用,这里并不会列举全部的 API 使用,仅列举一部分经常使用的,更详细的可参考 Node.js 中文网。
在 6.0.0 以前的 Node.js 版本中, Buffer 实例是使用 Buffer 构造函数建立的,该函数根据提供的参数以不一样方式分配返回的 Buffer new Buffer()
。
如今能够经过 Buffer.from()、Buffer.alloc() 与 Buffer.allocUnsafe() 三种方式来建立
Buffer.from()
const b1 = Buffer.from('10');
const b2 = Buffer.from('10', 'utf8');
const b3 = Buffer.from([10]);
const b4 = Buffer.from(b3);
console.log(b1, b2, b3, b4); // <Buffer 31 30> <Buffer 31 30> <Buffer 0a> <Buffer 0a>
复制代码
Buffer.alloc
返回一个已初始化的 Buffer,能够保证新建立的 Buffer 永远不会包含旧数据。
const bAlloc1 = Buffer.alloc(10); // 建立一个大小为 10 个字节的缓冲区
console.log(bAlloc1); // <Buffer 00 00 00 00 00 00 00 00 00 00>
复制代码
Buffer.allocUnsafe
建立一个大小为 size 字节的新的未初始化的 Buffer,因为 Buffer 是未初始化的,所以分配的内存片断可能包含敏感的旧数据。在 Buffer 内容可读状况下,则可能会泄露它的旧数据,这个是不安全的,使用时要谨慎。
const bAllocUnsafe1 = Buffer.allocUnsafe(10);
console.log(bAllocUnsafe1); // <Buffer 49 ae c9 cd 49 1d 00 00 11 4f>
复制代码
经过使用字符编码,可实现 Buffer 实例与 JavaScript 字符串之间的相互转换,目前所支持的字符编码以下所示:
const buf = Buffer.from('hello world', 'ascii');
console.log(buf.toString('hex')); // 68656c6c6f20776f726c64
复制代码
字符串转 Buffer
这个相信不会陌生了,经过上面讲解的 Buffer.form() 实现,若是不传递 encoding 默认按照 UTF-8 格式转换存储
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');
console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
复制代码
Buffer 转换为字符串
Buffer 转换为字符串也很简单,使用 toString([encoding], [start], [end]) 方法,默认编码仍为 UTF-8,若是不传 start、end 可实现所有转换,传了 start、end 可实现部分转换(这里要当心了)
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');
console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 9)); // Node.js �
复制代码
运行查看,能够看到以上输出结果为 Node.js �
出现了乱码,为何?
转换过程当中为何出现乱码?
首先以上示例中使用的默认编码方式 UTF-8,问题就出在这里一个中文在 UTF-8 下占用 3 个字节,技
这个字在 buf 中对应的字节为 8a 80 e6
而咱们的设定的范围为 0~9 所以只输出了 8a
,这个时候就会形成字符被截断出现乱码。
下面咱们改下示例的截取范围:
const buf = Buffer.from('Node.js 技术栈', 'UTF-8');
console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 11)); // Node.js 技
复制代码
能够看到已经正常输出了
在 Nodejs 中的 内存管理和 V8 垃圾回收机制 一节主要讲解了在 Node.js 的垃圾回收中主要使用 V8 来管理,可是并无提到 Buffer 类型的数据是如何回收的,下面让咱们来了解 Buffer 的内存回收机制。
因为 Buffer 须要处理的是大量的二进制数据,假如用一点就向系统去申请,则会形成频繁的向系统申请内存调用,因此 Buffer 所占用的内存再也不由 V8 分配,而是在 Node.js 的 C++ 层面完成申请,在 JavaScript 中进行内存分配。所以,这部份内存咱们称之为堆外内存。
注意:如下使用到的 buffer.js 源码为 Node.js v10.x 版本,地址:github.com/nodejs/node…
Node.js 采用了 slab 机制进行预先申请、过后分配,是一种动态的管理机制。
使用 Buffer.alloc(size) 传入一个指定的 size 就会申请一块固定大小的内存区域,slab 具备以下三种状态:
8KB 限制
Node.js 以 8KB 为界限来区分是小对象仍是大对象,在 buffer.js 中能够看到如下代码
Buffer.poolSize = 8 * 1024; // 102 行,Node.js 版本为 v10.x
复制代码
在 Buffer 初识 一节里有提到过 Buffer 在建立时大小已经被肯定且是没法调整的
到这里应该就明白了。
Buffer 对象分配
如下代码示例,在加载时直接调用了 createPool() 至关于直接初始化了一个 8 KB 的内存空间,这样在第一次进行内存分配时也会变得更高效。另外在初始化的同时还初始化了一个新的变量 poolOffset = 0 这个变量会记录已经使用了多少字节。
Buffer.poolSize = 8 * 1024;
var poolSize, poolOffset, allocPool;
... // 中间代码省略
function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeArrayBuffer(poolSize);
poolOffset = 0;
}
createPool(); // 129 行
复制代码
此时,新构造的 slab 以下所示:
如今让咱们来尝试分配一个大小为 2048 的 Buffer 对象,代码以下所示:
Buffer.alloc(2 * 1024)
复制代码
如今让咱们先看下当前的 slab 内存是怎么样的?以下所示:
那么这个分配过程是怎样的呢?让咱们再看 buffer.js 另一个核心的方法 allocate(size)
// https://github.com/nodejs/node/blob/v10.x/lib/buffer.js#L318
function allocate(size) {
if (size <= 0) {
return new FastBuffer();
}
// 当分配的空间小于 Buffer.poolSize 向右移位,这里得出来的结果为 4KB
if (size < (Buffer.poolSize >>> 1)) {
if (size > (poolSize - poolOffset))
createPool();
var b = new FastBuffer(allocPool, poolOffset, size);
poolOffset += size; // 已使用空间累加
alignPool(); // 8 字节内存对齐处理
return b;
} else { // C++ 层面申请
return createUnsafeBuffer(size);
}
}
复制代码
读完上面的代码,已经很清晰的能够看到什么时候会分配小 Buffer 对象,又什么时候会去分配大 Buffer 对象。
这块内容着实难理解,翻了几本 Node.js 相关书籍,朴灵大佬的「深刻浅出 Node.js」Buffer 一节仍是讲解的挺详细的,推荐你们去阅读下。
如下列举一些 Buffer 在实际业务中的应用场景,也欢迎你们在评论区补充!
关于 I/O 能够是文件或网络 I/O,如下为经过流的方式将 input.txt 的信息读取出来以后写入到 output.txt 文件,关于 Stream 与 Buffer 的关系不明白的在回头看下 Buffer 初识 一节讲解的 什么是 Stream?
、什么是 Buffer?
const fs = require('fs');
const inputStream = fs.createReadStream('input.txt'); // 建立可读流
const outputStream = fs.createWriteStream('output.txt'); // 建立可写流
inputStream.pipe(outputStream); // 管道读写
复制代码
在 Stream 中咱们是不须要手动去建立本身的缓冲区,在 Node.js 的流中将会自动建立。
zlib.js 为 Node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操做二进制数据流,提供了压缩或解压功能。参考源代码 zlib.js 源码
在一些加解密算法中会遇到使用 Buffer,例如 crypto.createCipheriv 的第二个参数 key 为 String 或 Buffer 类型,若是是 Buffer 类型,就用到了本篇咱们讲解的内容,如下作了一个简单的加密示例,重点使用了 Buffer.alloc() 初始化一个实例(这个上面有介绍),以后使用了 fill 方法作了填充,这里重点在看下这个方法的使用。
buf.fill(value[, offset[, end]][, encoding])
如下为 Cipher 的对称加密 Demo
const crypto = require('crypto');
const [key, iv, algorithm, encoding, cipherEncoding] = [
'a123456789', '', 'aes-128-ecb', 'utf8', 'base64'
];
const handleKey = key => {
const bytes = Buffer.alloc(16); // 初始化一个 Buffer 实例,每一项都用 00 填充
console.log(bytes); // <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
bytes.fill(key, 0, 10) // 填充
console.log(bytes); // <Buffer 61 31 32 33 34 35 36 37 38 39 00 00 00 00 00 00>
return bytes;
}
let cipher = crypto.createCipheriv(algorithm, handleKey(key), iv);
let crypted = cipher.update('Node.js 技术栈', encoding, cipherEncoding);
crypted += cipher.final(cipherEncoding);
console.log(crypted) // jE0ODwuKN6iaKFKqd3RF4xFZkOpasy8WfIDl8tRC5t0=
复制代码
缓冲(Buffer)与缓存(Cache)的区别?
缓冲(Buffer)
缓冲(Buffer)是用于处理二进制流数据,将数据缓冲起来,它是临时性的,对于流式数据,会采用缓冲区将数据临时存储起来,等缓冲到必定的大小以后在存入硬盘中。视频播放器就是一个经典的例子,有时你会看到一个缓冲的图标,这意味着此时这一组缓冲区并未填满,当数据到达填满缓冲区而且被处理以后,此时缓冲图标消失,你能够看到一些图像数据。
缓存(Cache)
缓存(Cache)咱们能够看做是一个中间层,它能够是永久性的将热点数据进行缓存,使得访问速度更快,例如咱们经过 Memory、Redis 等将数据从硬盘或其它第三方接口中请求过来进行缓存,目的就是将数据存于内存的缓存区中,这样对同一个资源进行访问,速度会更快,也是性能优化一个重要的点。
来自知乎的一个讨论,点击 more 查看
经过压力测试来看看 String 和 Buffer 二者的性能如何?
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);
复制代码
以上实例我放在虚拟机里进行测试,你也能够在本地电脑测试,使用 AB 测试工具。
测试 string
看如下几个重要的参数指标,以后经过 buffer 传输进行对比
$ ab -c 200 -t 60 http://192.168.6.131:3000/string
复制代码
测试 buffer
能够看到经过 buffer 传输总共的请求数为 50000、QPS 达到了两倍多的提升、每秒传输的字节为 9138.82 KB,从这些数据上能够证实提早将数据转换为 Buffer 的方式,可使性能获得近一倍的提高。
$ ab -c 200 -t 60 http://192.168.6.131:3000/buffer
复制代码
在 HTTP 传输中传输的是二进制数据,上面例子中的 /string 接口直接返回的字符串,这时候 HTTP 在传输以前会先将字符串转换为 Buffer 类型,以二进制数据传输,经过流(Stream)的方式一点点返回到客户端。可是直接返回 Buffer 类型,则少了每次的转换操做,对于性能也是有提高的。
在一些 Web 应用中,对于静态数据能够预先转为 Buffer 进行传输,能够有效减小 CPU 的重复使用(重复的字符串转 Buffer 操做)。
做者:五月君
连接:https://github.com/Q-Angelo/Nodejs-Roadmap
来源:Nodejs.js技术栈
复制代码