多数人都拥有本身不了解的能力和机会,都有可能作到不曾梦想的事情。 ——戴尔·卡耐基html
从前端转入 Node.js 的童鞋对这一部份内容会比较陌生,由于在前端中一些简单的字符串操做已经知足基本的业务需求,有时可能也会以为 Buffer、Stream 这些会很神秘。回到服务端,若是你不想只作一名普通的 Node.js 开发工程师,你应该深刻去学习一下 Buffer 揭开这一层神秘的面纱,同时也会让你对 Node.js 的理解提高一个水平。前端
做者简介:五月君,Nodejs Developer,热爱技术、喜欢分享的 90 后青年,公众号 “Nodejs技术栈”,Github 开源项目 www.nodejs.rednode
在引入 TypedArray 以前,JavaScript 语言没有用于读取或操做二进制数据流的机制。 Buffer 类是做为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操做、以及其余上下文中与八位字节流进行交互。这是来自 Node.js 官网的一段描述,比较晦涩难懂,总结起来一句话 Node.js 能够用来处理二进制流数据或者与之进行交互。git
Buffer 用于读取或操做二进制数据流,作为 Node.js API 的一部分使用时无需 require,用于操做网络协议、数据库、图片和文件 I/O 等一些须要大量二进制数据的场景。Buffer 在建立时大小已经被肯定且是没法调整的,在内存分配这块 Buffer 是由 C++ 层面提供而不是 V8 具体后面会讲解。github
在这里不知道你是否定为这是很简单的?可是上面提到的一些关键词二进制
、流(Stream)
、缓冲区(Buffer)
,这些又都是什么呢?下面尝试作一些简单的介绍。算法
谈到二进制咱们大脑可能会浮想到就是 010101 这种代码命令,以下图所示:数据库
正如上图所示,二进制数据使用 0 和 1 两个数码来表示的数据,为了存储或展现一些数据,计算机须要先将这些数据转换为二进制来表示。例如,我想存储 66 这个数字,计算机会先将数字 66 转化为二进制 01000010 表示,印象中第一次接触这个是在大学期间 C 语言课程中,转换公式以下所示:api
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 像管道同样,一点一点的将数据流出。
举个例子
咱们如今有一大罐水须要浇一片菜地,若是咱们将水罐的水一下所有倒入菜地,首先得须要有多么大的力气(这里的力气比如计算机中的硬件性能)才可搬得动。若是,咱们拿来了水管将水一点一点流入咱们的菜地,这个时候不要这么大力气就可完成。
经过上面的讲解进一步的理解了 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 操做)。
欢迎你们关注「Nodejs技术栈」公众号,扫描关注我哦!