什么是二进制数据?
了解Buffer之前,我们先来了解一下,什么是二进制数据。二进制数据是由0和1两个数字组成的。计算机用0和1的不同组合来表示不同的数据,为了存储和展示数据,计算机需要将不同的数据转为二进制数据;例如66这个数字,计算机在读取时会将66转为二进制 01000010 表示。例如我们对一个英文 M 操作,在 JavaScript 里通过 'M'.charCodeAt() 取到对应的 ASCII 码之后(通过以上的步骤)会转为二进制表示。不单单对于数字,字母,其他格式如文件、图像、字符串等也都有一套对应的规则转为二进制数据被计算机处理。
什么是stream(流)?
流是对输入输出设备的一个抽象理解。这里的设备可以是文件、网络、内存等等。
流是有方向性的,当我们从服务器读取一个文件时,那么服务器就会开启一个流,将文件一点一点由服务器流向客户端;当我们从客户端发送较大的文件给服务器是,那么就会从客户端开启一个输出流,我们就需要 Stream 像管道一样,一点一点的将数据流出。
举个例子:我们现在有一大罐水需要浇一片菜地,如果我们将水罐的水一下全部倒入菜地,首先得需要有多么大的力气(这里的力气好比计算机中的硬件性能)才可搬得动。如果,我们拿来了水管将水一点一点流入我们的菜地,这个时候不要这么大力气就可完成。
什么是Buffer?
我们已经知道stream(流)的概念,就是数据从一端流向一端的过程。那么是如何流动的呢?
通常,数据的流动是为了读取或者操作它,并对它进行决策。伴随着时间得推移,每个过程都会有一个最大的量或者最小量,如果数据到达的速度比消耗的速度快,那么少数早到达的数据就会处于等候区,暂时没有处理;反之,数据到达的速度比进程慢,那么早先到达的数据需要等到一定量的数据到达后才能被处理。
这里的等候区,就是指的是缓冲区(Buffer),它是计算机中的一个小物理单位,通常位于计算机的RAM中
比放说去公园要玩儿一个过山车:每十分钟发一次车,需要排队等够20个人才能开始,一开始人很多35个人,那么后来的15个人就得在等五个人才能开始。到了下一次发车时间,发现人还没满,就等到人满之后在发车。
这里的等待过山车发车的等候区,对应到我们Node.js中的缓冲区Buffer。乘客到达的速度我们是不能控制的,我们能控制的只是发车的时间间隔。对应到程序中就是我们无法控制数据流到达的时间,可以做到的是何时发送数据
总的来说:Buffer 类是作为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。这是来自 Node.js 官网的一段描述,比较晦涩难懂,总结起来一句话 Node.js 可以用来处理二进制流数据或者与之进行交互。
Buffer 用于读取或操作二进制数据流,做为 Node.js API 的一部分使用时无需 require,用于操作网络协议、数据库、图片和文件 I/O 等一些需要大量二进制数据的场景。Buffer 在创建时大小已经被确定且是无法调整的,在内存分配这块 Buffer 是由 C++ 层面提供而不是 V8 具体后面会讲解。
Buffer的基本使用
-
创建Buffer
可以通过 Buffer.from()、Buffer.alloc()、Buffer.allocUnsafe() 三种方式来创建
Buffer.form()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类型互转
-
字符串转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); // 17console.log(buf.toString('UTF-8', 0, 9)); // Node.js �
这里出现乱码是因为UTF-8编码格式时,一个中文就占用3个字节,这里只提供0-9,,因此只输出了8a,这个时候就造成了字符换被截断出现乱码
Buffer的内存机制
由于Buffer需要处理大量的二进制数据,假如用一点就像系统去申请,就会造成频繁的向,系统申请内存调用。所以Buffer所占用的内存不再由V8分配;而是在Node.js的C++层面完成申请,在JavaScript层面中进行内存分配。因此,这部分内存我们称之为堆外内存。
-
Buffer内存分配原理
Node.js采用了slab机制进行预先申请,事后分配,是一种动态管理机制。
使用 Buffer.alloc(size) 传入一个指定的 size 就会申请一块固定大小的内存区域,slab 具有如下三种状态:
full:完全分配状态
partial:部分分配状态
empty:没有被分配状态 -
8K限制
Node.js 以 8KB 为界限来区分是小对象还是大对象,在 buffer.js 中可以看到以下代码Buffer.poolSize = 8 * 1024; // 102 行,Node.js 版本为 v10.x -
Buffer对象分配
以下代码的示例:在加载时使用了createPool()方法相当于初始化了一个8KB的内存空间,这样在第一次进行内存分配时也会变得高效,初始化时还创建了另一个对象poolOffset ,这个变量会记录已经使用了多少内存空间。Buffer.poolSize = 8 * 1024 var poolSize,poolOffset,allocPool ....... 中间代码省略 function createPool(){ poolSize = Buffer.poolSize; allocPool = createUnsafeArrayBuffer(poolSize) poolOffset = 0; } createPool() // //此时,新构造的flab如下图所示
现在我们尝试分配一个空间大小为2KB的buffe对象
Buffer.alloc(2 * 1024 )
分配过后的内存展示如下图
这个分配过程是怎样的呢?这时用到了Buffer的另一个核心方法 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 // 已使用空间累加
alignOPool() // 8字节内存处理
}else{ //C++ 层面申请
return createUnsafeBuffer(size);
}
}
Buffer内存分配总结
- 在初次加载时,会初始化一个8KB大小的空间(Buffer.js源码有体现)
- 根据申请的内存情况分为**小Buffer对象和大Buffer对象**
- 小Buffer情况,会继续判断slab空间是否足够
如果空间足够会使用剩余空间,同时更新slab状态,偏移量增加
如果空间不足,slab空间不足,就会创建一个新的slab空间来进行分配 - 大Buffer情况会直接向C++层面申请,调用createUnsafeBuffer(size) 函数
- 无论是小Buffer还是大Buffer,内存分配都是在C++层面上完成,内存管理在JavaScript层,最终还可以被V8的垃圾回收机制回收。
Buffer应用场景
-
I/O操作
关于I/O操作,可以是网络I/O,也可以是文件的I/O,以下案例通过流的方式将input.txt信息读取出来之后写入到output文件中const fs = require('fs'); // 引入文件读取模块const inputStream = fs.createReadStream('input.txt'); // 创建可读流const outputStream = fs.createWriteStream('output.txt'); // 创建可写流inputStream.pipe(outputStream); // 管道读写 -
zlib.js
zlib.js 为 Node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操作二进制数据流,提供了压缩或解压功能。参考源代码 zlib.js 源码
Buffer vs Cache
-
缓冲(Buffer)
缓冲(Buffer)是用于处理二进制流数据,将数据缓冲起来,它是临时性的,对于流式数据,会采用缓冲区将数据临时存储起来,等缓冲到一定的大小之后在存入硬盘中。视频播放器就是一个经典的例子,有时你会看到一个缓冲的图标,这意味着此时这一组缓冲区并未填满,当数据到达填满缓冲区并且被处理之后,此时缓冲图标消失,你可以看到一些图像数据。 -
缓存(Cache) 缓存(Cache)我们可以看作是一个中间层,它可以是永久性的将热点数据进行缓存,使得访问速度更快,例如我们通过 Memory、Redis 等将数据从硬盘或其它第三方接口中请求过来进行缓存,目的就是将数据存于内存的缓存区中,这样对同一个资源进行访问,速度会更快,也是性能优化一个重要的点。
总结
Buffer是Node.js中最为重要的核心模块之一,使用时无需引入,可以直接使用。本文借鉴了五月君的文章,记录自己的学习Node.js的过程,希望和大家一起学习Node.js