Node.js中Buffer的一些实现原理

news2024/11/24 16:04:07

1.前言

在ES6之前,JavaScript无法直接处理二进制数据,Node.js为了弥补这个不足引入了 Buffer,其是Node.js的核心模块之一,底层实现基于C++。本文将从 Node.js v14.20.0 的源码分析 Buffer 的一些实现原理。

2.ArrayBuffer

在介绍 Buffer 之前,必须花一点时间介绍 ArrayBuffer。

2.1.简介

根据 ECMAScript 6 入门 中的描述,ArrayBuffer 对象、TypedArray 视图和 DataView 视图是 JavaScript 操作二进制数据的一个接口。其设计目的与WebGL有关,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,如果不采用二进制,则 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。
二进制数组由三类对象组成:

  • ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
  • TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
  • DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。

简单来说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

2.2.使用

ArrayBuffer有两种使用方式——通过DataVIew 和 通过 TypedArray。

2.2.1.通过DataVIew

通过 DataVIew 使用 ArrayBuffer 参考以下代码:

const buf = new ArrayBuffer(32)
const dataView = new DataView(buf)
dataView.getUint8(0) // 0

2.2.1.通过TypedArray

通过 TypedArray使用 ArrayBuffer 参考以下代码:

const buf1 = new ArrayBuffer(10)
const x1 = new Uint8Array(buf1)
x1[0]  = 123

TypedArray的构造函数主要有两种:

  1. TypedArray(buffer, byteOffset = 0, length?):在同一个ArrayBuffer对象之上,可以根据不同的数据类型,建立多个视图,其中三个参数分别是:视图对应的底层ArrayBuffer对象;视图开始的字节序号,默认从 0 开始;视图包含的数据个数,默认直到本段内存区域结束。
  2. TypedArray(length):不通过ArrayBuffer对象,直接分配内存而生成。

如果已经有了一个 ArrayBuffer 对象,使用第一个构造函数明显会比较高效一些,请记住这点,后续会用到。

3.Buffer

3.1.简介

Buffer 实例也是 Uint8Array 实例, Uint8Array则是 TypedArray 的子类。 因此,所有 TypedArray 的方法在 Buffer 上也可用。 但是 Buffer 的 API 和 TypedArray 的 API 之间存在细微的不兼容。
Buffer和ArrayBuffer都用于处理二进制数据,但它们有以下区别:

  • 实现方式不同:Buffer是Node.js的核心模块,实现方式基于C++,性能非常高;而ArrayBuffer是JavaScript的内置对象,由JavaScript虚拟机提供支持。
  • 可读性不同:在通过控制台打印时,Buffer打印的是十六进制格式的数据,不够直观;而ArrayBuffer则更容易理解。
  • 支持功能不同:Buffer提供了一系列API用于操作二进制数据,如截取、转化、比较、拷贝等;而ArrayBuffer提供的功能相对简单,需要借助TypedArray和DataView来进行操作。

3.2.实例化

在 Node.js v6 之前都是通过调用构造函数的方式实例化 Buffer,根据参数返回不同结果。处于安全性原因,这种方式在 v6 后的版本中已经被废除,现在提供了四个职责清晰的函数处理实例化 Buffer 的工作。
实例化buffer涉及到多个函数和类,其之间的调用关系如图:
在这里插入图片描述注意,上图只是调用关系,而非实际流程,比如 Buffer.alloc 方法调用 createUnsafeBuffer 之后还会对创建的 Buffer 对象进行填充。

3.2.1.Buffer.from

Buffer.from 支持四种参数类型:

  • Buffer.from(string [, encoding]):返回一个包含给定字符串的 Buffer。
  • Buffer.from(buffer):返回给定 Buffer 的一个副本 Buffer。
  • Buffer.from(array):返回一个内容包含所提供的字节副本的 Buffer,数组中每一项是一个表示八位字节的数字,所以值必须在 0 ~ 255 之间,否则会取模。
  • Buffer.from(arrayBuffer):返回一个与给定的 ArrayBuffer 共享内存的新 Buffer。
  • Buffer.from(object[, offsetOrEncoding[, length]]):取 object 的 valueOf 或 Symbol.toPrimitive 初始化 Buffer。

具体细节参考 Buffer静态方法。
例如:

const buf1 = Buffer.from('test', 'utf-8'); // <Buffer 74 65 73 74>
const buf3 = Buffer.from([256, 2, 3]); // <Buffer 00 02 03>

当输入一个大于255或小于0时,会进行mod 256运算,mod 256 表示将一个数除以256后取余数的操作,因为单个字节的取值范围是 0 ~ 255,因此可以利用 mod 256 运算,将其转化为 0 ~ 255 的范围内的值。
对应源码:

// lib/buffer.js
Buffer.from = function from(value, encodingOrOffset, length) {
  if (typeof value === 'string')
    return fromString(value, encodingOrOffset);

  if (typeof value === 'object' && value !== null) {
    if (isAnyArrayBuffer(value))
      return fromArrayBuffer(value, encodingOrOffset, length);

    const valueOf = value.valueOf && value.valueOf();
    if (valueOf != null &&
        valueOf !== value &&
        (typeof valueOf === 'string' || typeof valueOf === 'object')) {
      return from(valueOf, encodingOrOffset, length);
    }

    const b = fromObject(value);
    if (b)
      return b;

    if (typeof value[SymbolToPrimitive] === 'function') {
      const primitive = value[SymbolToPrimitive]('string');
      if (typeof primitive === 'string') {
        return fromString(primitive, encodingOrOffset);
      }
    }
  }

  throw new ERR_INVALID_ARG_TYPE(
    'first argument',
    ['string', 'Buffer', 'ArrayBuffer', 'Array', 'Array-like Object'],
    value
  );
};

各种from函数最终都是调用FastBuffer,FastBuffer 继承自 Uint8Array,FastBuffer 会在后续介绍。

3.2.2.Buffer.alloc

使用 Buffer.alloc(size [, fill [, encoding]]) 分配一个大小为 size 字节的新 Buffer,如果 fill 为 undefined,则用 0 填充 Buffer,参数含义如下:

  • size:integer,表示新 Buffer 的所需长度。
  • fill:string | Buffer | Uint8Array | integer,用于预填充新 Buffer 的值,默认值: 0。
  • encoding:string,如果 fill 是一个字符串,则这是它的字符编码,默认值: utf8。

例如:

const buf1 = Buffer.alloc(5);
console.log('buf1', buf1); // <Buffer 00 00 00 00 00>

const buf2 = Buffer.alloc(5, 'a');
console.log('buf2', buf2); // <Buffer 61 61 61 61 61>

对应源码:

// lib/buffer.js
Buffer.alloc = function alloc(size, fill, encoding) {
  assertSize(size);
  if (fill !== undefined && fill !== 0 && size > 0) {
    const buf = createUnsafeBuffer(size);
    return _fill(buf, fill, 0, buf.length, encoding); // _fill方法用于填充 Buffer
  }
  return new FastBuffer(size);
};

关于 createUnsafeBuffer 的具体细节,也会在后面介绍。

3.2.3.Buffer.allocUnsafe(size)

分配一个大小为 size 字节的新 Buffer,allocUnsafe 执行速度比 alloc 快,因为理想情况下 allocUnsafe 是从已有的ArrayBuffer中分配内存而不是直接创建Buffer对象,但由于它不会清空所分配的内存,可能包含敏感数据的残留,例如:

// allocUnsafe不安全示例
const buf3 = Buffer.allocUnsafe(20);
buf3.write('my secret information!');
console.log('buf3', buf3);

const buf4 = Buffer.allocUnsafe(20);
console.log('buf4', buf4); // buf4可能包含了一些数据

对应源码:

// lib/buffer.js
Buffer.allocUnsafe = function allocUnsafe(size) {
  assertSize(size);
  return allocate(size);
};

关于 allocate 函数的具体细节,也会在后面介绍。

3.2.4.Buffer.allocUnsafeSlow(size)

allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域,对应源码:

Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
  assertSize(size);
  return createUnsafeBuffer(size);
};

3.3.关键实现

上文遗留了三个关键的类/方法:FastBuffer类、createUnsafeBuffer方法、allocate方法,本节将介绍这三个关键类/方法的实现。

3.3.1.FastBuffer

FastBuffer 继承自 Uint8Array,也就是说FastBuffer 也是 TypedArray:

// lib/internal/buffer.js
class FastBuffer extends Uint8Array {
  constructor(bufferOrLength, byteOffset, length) {
    super(bufferOrLength, byteOffset, length);
  }
}

3.3.2.createUnsafeBuffer

上面实例化的几种方式或多或少都用到了 createUnsafeBuffer,其源码如下:

// lib/buffer.js
const zeroFill = bindingZeroFill || [0];

function createUnsafeBuffer(size) {
  zeroFill[0] = 0;
  try {
    return new FastBuffer(size);
  } finally {
    zeroFill[0] = 1;
  }
}

createUnsafeBuffe r内部在实例化 FastBuffer 之前,将 zeroFill[0] 设置为 0, createUnsafeBuffer 与直接实例化一个 FastBuffer 对象的区别就在于此。
zeroFill[0] 是控制零填充行为的开关,zeroFill[0] 为 0 时关闭零填充, zeroFill[0] 为 1 时打开零填充。当开关关闭时,不对新实例化的 Buffer 对象进行默认填充 0,因此可能包含敏感数据的残留,所以说 createUnsafeBuffer 是不安全的。

3.3.3.allocate

allocate源码如下:

// lib/buffer.js
Buffer.poolSize = 8 * 1024;

// allocPool实际就是一个ArrayBuffer对象,当满足一定条件时,会通过这个ArrayBuffer生成新的FastBuffer
// 这三个变量分别表示 分配池的大小、分配池的位移量、分配池本身
let poolSize, poolOffset, allocPool; 

function createPool() {
  poolSize = Buffer.poolSize;
  // createUnsafeBuffer返回一个FastBuffer实例,也就是一个TypedArray实例
  // TypedArray实例的buffer属性指向原始的ArrayBuffer对象
  // 也就是说allocPool是一个ArrayBuffer对象
  allocPool = createUnsafeBuffer(poolSize).buffer;
  markAsUntransferable(allocPool);
  poolOffset = 0;
}

// lib/buffer.js加载时就会申请一个 8KB 的内存空间
createPool();

// ...

function allocate(size) {
  if (size <= 0) {
    return new FastBuffer(); // size小于0是非法情况,返回一个空的FastBuffer对象避免程序出错
  }
  
  // >>> 是一个无符号右移运算符,右移一位相当于除以2
  // 这里是判断需要分配的内存大小是否小于对象池大小的一半,即是否小于4kb
  if (size < (Buffer.poolSize >>> 1)) {
    // 这里判断需要分配的内存大小是否大于对象池中剩余空间的大小
    // 当大于成立时,对象池已经没有足够的空间存储 size 大小的数据,会调用 createPool() 函数创建一个新的分配池
    if (size > (poolSize - poolOffset))
      createPool(); // createPool()执行完成之后,allocPool指向新的分配池
    const b = new FastBuffer(allocPool, poolOffset, size); // 如果没有创建新的分配池,则会复用之前的分配池来创建新的Buffer
    poolOffset += size; // 记录偏移量
    alignPool(); // 将池的偏移量对齐到 8 字节的倍数
    return b;
  }
  
  // 如果需要分配的内存大小不小于分配池大小的一半,则直接使用 createUnsafeBuffer() 函数创建一个新的非安全 Buffer 对象
  // 这样做的原因是:如果分配的内存比对象池中剩余的内存块还要大,就需要额外分配一块新的内存来创建 Buffer 对象。如果这类操作频繁发生,就会导致内存的浪费
  return createUnsafeBuffer(size);
}

allocate 方法的核心思想是 Slab 分配机制。

3.3.3.1.Slab 分配机制

allocate使用allocPool的动机是想更快的创建Buffer对象,为此,Node采用了 Slab 分配机制分配内存,Slab 是一种动态内存管理机制,其基本原理是先申请好一块固定大小的内存区域,当需要分配内存时,就从这个实现申请好的内存区域中划分一块区域出来存储对应的数据,这做可以避免频繁申请内存造成的开销。
具体到Node中,就是基于已有的ArrayBuffer对象(allocPool)创建TypedArray对象(FastBuffer),但是具体的实现要复杂一些。
理想情况下,allocPool应该得到充分利用,即allocPool中保存的Buffer对象都紧密的挨在一起,将8kb的内存空间完全利用,但实际情况是需要创建的Buffer对象有大有小,经过多次分配之后,allocPool的剩余空间不足以再保存新的Buffer对象,此时就需要申请新的内存空间(创建新的ArrayBuffer对象),原来的剩余空间就被浪费掉了,如果申请新空间的操作越频繁,被浪费的空间就越多,并且申请新空间是需要开销的。
为解决这个问题,在 allocate 中以4kb为界,当待分配内存小于4kb时,allocUnsafe 并没有直接创建新的Buffer,而是从allocPool中分配一块区域来保存Buffer,如果剩余空间不够,再开辟一块新的空间用来保存Buffer;但当分配的内存大于等于4kb时,allocUnsafe会直接返回FastBuffer,而不是基于已有的allocPool。
借用《深入浅出Node.js》中的话:

真正的内存是在Node的C++层面提供的,JavaScript层面只是使用它。当进行小而频繁的Buffer操作时,采用slab的机制进行预先申请和事后分配,使得JavaScript到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的Buffer而言,则直接使用C++层面提供的内存,而无需细腻的分配操作。

4.参考

https://es6.ruanyifeng.com/#docs/arraybuffer
https://nodejs.cn/api/buffer.html
极简Node.js入门教程-Buffer
【node 源码学习笔记】buffer 缓存区
Node.js 中的缓冲区(Buffer)究竟是什么?
Buffer.createUnsafeBuffer和alloc都用了FastBuffer 为什么前者安全后者不安全
深入浅出node.js

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/564962.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Visual Studio插件DevExpress CodeRush v22.1- 支持C# 10

DevExpress CodeRush是一个强大的Visual Studio .NET 插件&#xff0c;它利用整合技术&#xff0c;通过促进开发者和团队效率来提升开发者体验。为Visual Studio IDE增压、消除重复的代码并提高代码质量&#xff0c;可以快速思考、自动化测试、可视化调试和重构。 CodeRush v2…

资深程序员深度体验ChatGPT一周发现竟然....

周一打卡上班&#xff0c;老板凑到我跟前&#xff1a;“小李啊&#xff0c;这周有个新需求交给你做一下&#xff0c;给我们的API管理平台新增一个智能Mock的功能...”。我条件反射般的差点脱口而出&#xff1a;“这个需求做不了..”。不过在千钧一发之间&#xff0c;我想起了最…

【文章学习系列之技巧】Network Slimming

本章内容 文章概况问题来源方法实验结果总结 文章概况 这是一篇2017年发表于ICCV的一篇论文。该论文指出深度卷积神经网络的应用受到了高计算成本的阻碍&#xff0c;并提出一种修剪模型结构的方式用于降低这种成本&#xff0c;使得模型大小减小、运行内存减小且不降低精度的情…

http\https协议

前言 小亭子正在努力的学习编程&#xff0c;接下来将开启javaEE的学习~~ 分享的文章都是学习的笔记和感悟&#xff0c;如有不妥之处希望大佬们批评指正~~ 同时如果本文对你有帮助的话&#xff0c;烦请点赞关注支持一波, 感激不尽~~ 目录 前言 一、 认识http协议 1.概念 1.1…

ChatGPT APP Plus升级全记录:购买礼品卡、兑换和处理失败

大家好&#xff0c;我是可夫小子&#xff0c;《小白玩转ChatGPT》专栏作者&#xff0c;关注AIGC、读书和自媒体。 在上一篇《ChatGPT APP来了&#xff0c;支持语音输入&#xff0c;还可以直接订阅Plus账号》文章中&#xff0c;我介绍了ChatGPT App下载安装教程。本文主要介绍怎…

YOLO中的值得借鉴的思想

关键理论的理解&#xff0c;后面会补充结构等。 1.YOLO1中将图像划分为7*7个网格&#xff0c;每个网格都预测网格中的的类别&#xff08;是什么物体&#xff09;&#xff0c;以及预测到的物体所对应的框&#xff08;四个位置量&#xff0c;一个置信度&#xff09;&#xff0c;所…

一、尚医通预约下单

文章目录 一、预约下单1、需求分析1.1订单表结构1.2下单分析 2、搭建service-order模块2.1 搭建service-order模块2.2 修改配置2.3 启动类2.4配置网关 3、添加订单基础类3.1 添加model3.2 添加Mapper3.3 添加service接口及实现类3.4 添加controller 4、封装Feign调用获取就诊人…

【Redis】聊一下Redis事务以及watch机制

我们知道熟悉MySQL的同学&#xff0c;一定了解ACID属性。ACID分别对应四种属性&#xff0c;但是Redis的事务和ACID属性有什么不一样的地方嘛&#xff0c;我们来深入探讨下。 Redis事务和MySQL事务的区别 ACID的本质是保证了事务执行前后对结果的保证&#xff0c;以及数据状态…

二、数据结构2:双链表 模板题+算法模板(双链表)

文章目录 算法模板双链表题目模板 模板题双链表原题链接题目思路题解 算法模板 双链表题目模板 // e[]表示节点的值&#xff0c;l[]表示节点的左指针&#xff0c;r[]表示节点的右指针&#xff0c;idx表示当前用到了哪个节点 int e[N], l[N], r[N], idx;// 初始化 void init()…

Android进阶 View事件体系(一):概要介绍和实现View的滑动

Android进阶 View事件体系&#xff08;一&#xff09;&#xff1a;概要介绍和实现View的滑动 内容概要 本篇文章为总结View事件体系的第一篇文章&#xff0c;将介绍的内容主要有&#xff1a; 什么是View和ViewGroupAndroid中View的坐标轴手势检测和速度检测如何实现View的滑动…

【ZYNQ】ZYNQ7000 UART 控制器及驱动应用示例

UART 简介 我们在使用 PS 的时候&#xff0c;通常会添加 UART 控制器&#xff0c;用于打印信息和调试代码。除此之外&#xff0c;PS 在和外 部设备通信时&#xff0c;也会经常使用串口进行通信。 UART 控制器 UART 控制器是一个全双工异步收发控制器&#xff0c;ZYNQ 内部包…

ssm实现发送邮箱功能

参考&#xff1a;ssm整合JavaMail发送邮件_ssm整合mimemessage_ds_surk的博客-CSDN博客 我在这位前辈写的博客的基础上进行讲解完善&#xff0c;避免踩坑。 我的jdk版本&#xff1a;1.8.0_333 1、引入依赖 相信很多朋友都卡在这里&#xff1a; 1、没有JavaMailSenderImpl类 2、…

数字逻辑 期末

概述 教材&#xff1a;《电子技术基础&#xff08;数字部分&#xff09;》 第五版 7400系列是TTL型芯片&#xff0c;商用型 数制 十进制->二进制 除2取余法&乘2取整法&#xff08;注意精度&#xff0c;但计科简单不考&#xff09; 十六进制->二进制 一位变四位 八…

Linux下串口编程

Linux下串口编程 Linux下的串口编程是通过串口设备文件和串口通信的系统调用函数来实现的。Linux下的串口设备文件通常为/dev/ttyS或/dev/ttyUSB(*表示数字),这些设备文件代表了对应的串口硬件设备。 在进行串口编程之前,需要先打开并初始化串口设备,其中包括设置波特率…

Linux 在桌面添加快捷启动图标(可添加至收藏夹)

0 背景 在 Ubuntu 系统下启动程序一般在 Terminal 通过输入指令启动&#xff0c;如 ./cfw。对于常用的程序&#xff0c;为了方便&#xff0c;创建桌面快捷图标 .desktop。为了让图标能够添加在收藏栏中&#xff0c;将 .desktop 融入桌面环境。 1 创建 .desktop 文件 参考&…

dvwa靶场通关(一)

第一关&#xff1a;Brute force low 账号是admin&#xff0c;密码随便输入 用burp suite抓包 爆破得出密码为password 登录成功 Medium 中级跟low级别基本一致&#xff0c;分析源代码我们发现medium采用了符号转义&#xff0c;一定程度上防止了sql注入&#xff0c;采用暴力破…

如何成为一名黑客?小白必学的12个基本步骤

黑客攻防是一个极具魅力的技术领域&#xff0c;但成为一名黑客毫无疑问也并不容易。你必须拥有对新技术的好奇心和积极的学习态度&#xff0c;具备很深的计算机系统、编程语言和操作系统知识&#xff0c;并乐意不断地去学习和进步。 如果你想成为一名优秀的黑客&#xff0c;下…

大项目参考地址​编辑 大项目接口实现

目录 大项目参考地址​编辑 口语考试 纸笔口语考试通常会安排在笔试前一周至笔试后一周的任意一天&#xff0c;机考口语考试通常会安排在笔试当天或者与笔试日期尽可能相邻的日期。根据考务安排的需要&#xff0c;在特殊情况下&#xff0c;口试日期有可能超出此区间&#xff0…

Java——《面试题——多线程并发篇》

前文 java——《面试题——基础篇》 Java——《面试题——JVM篇》 目录 前文 1、说说Java中实现多线程有几种方法 2、如何停止一个正在运行的线程 3、notify()和notifyAll()有什么区别&#xff1f; 4、sleep()和wait() 有什么区别&#xff1f; 5、volatile 是什么?可…

nodejs+vue网络课程在线考试系统an7ib

在线考试系统的设计与实现主要实现角色有管理员和用户,管理员在后台管理学生模块、用户表模块、token表模块、考试资讯模块、考试记录表模块、试题表模块、试卷表模块、配置文件模块、在线答疑模块 采用了Windows10操作系统平台&#xff0c;使用vue前端模板node作为后台监控&am…