从零开始学Node.js(三、缓冲区Buffer究竟是什么)

从零开始学Node.js(三、缓冲区Buffer究竟是什么)

技术杂谈小彩虹2021-07-24 18:16:46380A+A-

什么是二进制数据?

了解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内存分配总结

  1. 在初次加载时,会初始化一个8KB大小的空间(Buffer.js源码有体现)
  2. 根据申请的内存情况分为**小Buffer对象大Buffer对象**
  3. 小Buffer情况,会继续判断slab空间是否足够
    如果空间足够会使用剩余空间,同时更新slab状态,偏移量增加
    如果空间不足,slab空间不足,就会创建一个新的slab空间来进行分配
  4. 大Buffer情况会直接向C++层面申请,调用createUnsafeBuffer(size) 函数
  5. 无论是小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

参考文章

Node.js中的缓冲区(Buffer)究竟是什么?

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们