• 技术文章 >web前端 >js教程

    深入了解Nodejs中的buffer缓存区

    青灯夜游青灯夜游2021-07-22 10:12:47转载95
    本篇文章带大家了解一下NodeJs中的buffer缓存区,介绍一下Node.js中Buffer的实现,一起来看看吧!

    涉及的知识点

    ArrayBuffer

    先说一下 JavaScript 中的 ArrayBuffer 的接口及其背景, 如下内容来自于 ECMAScript 6 入门 ArrayBuffer

    ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。

    这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。

    二进制数组就是在这种背景下诞生的。它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 JavaScript 处理二进制数据的能力,使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信。

    看完我们知道, ArrayBuffer 系列接口使得 JavaScript 有了处理二进制数据的能力, 其使用方式主要是分为如下几步

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

    Buffer

    在 Node.js 中也完全可以使用 ArrayBuffer 相关的接口去处理二进制数据, 仔细看完 ArrayBufferBuffer 的文档可以发现, Buffer 的进一步封装能够更简单的上手与更好的性能, 接着让我们去看看 Buffer 的使用例子

    1. 通过 alloc 方法创建长度为 10 的内存区
    2. 通过 writeUInt8 向第一个字节写入数据 123
    3. 通过 readUint8 读取第一个字节的数据
    const buf1 = Buffer.alloc(10);
    buf1.writeUInt8(123, 0)
    buf1.readUint8(0)

    Buffer.alloc

    通过静态方法 alloc 创建一个 Buffer 实例

    Tips: 直接通过 Buffer 构造函数创建实例的方式由于安全性问题已经废弃

    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);
      }
      return new FastBuffer(size);
    };
    
    class FastBuffer extends Uint8Array {
      constructor(bufferOrLength, byteOffset, length) {
        super(bufferOrLength, byteOffset, length);
      }
    }

    发现 Buffer 其实就是 Uint8Array, 这里再补充一下在 JavaScript 中也可以不通过 ArrayBuffer 对象, 直接使用 Uint8Array 操作内存, 如以下的例子

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

    那么 Node.js 中 Buffer 仅通过 Uint8Array 类, 如何模拟实现下面所有的视图类型的行为, 以及 Buffer 又做了哪些其他的扩展了 ?

    allocUnsafe, allocUnsafeSlow

    提供了 alloc, allocUnsafe, allocUnsafeSlow 3个方法去创建一个 Buffer 实例, 上面讲了 alloc 方法没有什么特别, 下面讲一下另外两种方法

    allocUnsafe

    与 alloc 不同的是, allocUnsafe 并没有直接返回 FastBuffer, 而是始终从 allocPool 中类似 slice 出来的内存区。

    Buffer.allocUnsafe = function allocUnsafe(size) {
      assertSize(size);
      return allocate(size);
    };
    
    function allocate(size) {
      if (size <= 0) {
        return new FastBuffer();
      }
      if (size < (Buffer.poolSize >>> 1)) {
        if (size > (poolSize - poolOffset))
          createPool();
        const b = new FastBuffer(allocPool, poolOffset, size);
        poolOffset += size;
        alignPool();
        return b;
      }
      return createUnsafeBuffer(size);
    }

    这块内容其实我也是很早之前在读朴灵大佬的深入浅出 Node.js 就有所映像, 为什么这样做了, 原因主要如下

    为了高效地使用申请来的内存,Node采用了slab分配机制。slab是一种动态内存管理机制,最早

    诞生于SunOS操作系统(Solaris)中,目前在一些*nix操作系统中有广泛的应用,如FreeBSD和Linux。 简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态。

    当我们需要一个Buffer对象,可以通过以下方式分配指定大小的Buffer对象:

    new Buffer(size); Node以8 KB为界限来区分Buffer是大对象还是小对象: Buffer.poolSize = 8 * 1024; 这个8 KB的值也就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配。

    allocUnsafeSlow

    比起 allocUnsafe 从预先申请好的 allocPool 内存中切割出来的内存区, allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域。从命名可知直接使用 Uint8Array 等都是 Slow 缓慢的。

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

    createUnsafeBuffer

    这个 Unsafe 不安全又是怎么回事了, 其实我们发现直接通过 Uint8Array 申请的内存都是填充了 0 数据的认为都是安全的, 那么 Node.js 又做了什么操作使其没有被填充数据了 ?

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

    那么我们只能去探究一下 zeroFill 在创建前后, 类似开关的操作的是如何实现这个功能

    getZeroFillToggle

    zeroFill 的值来自于 getZeroFillToggle 方法返回, 其实现在 src/node_buffer.cc 文件中, 整个看下来也是比较费脑。

    简要的分析一下 zeroFill 的设置主要是修改了 zero_fill_field 这个变量的值, zero_fill_field 值主要使用在 Allocate 分配器函数中。

    void GetZeroFillToggle(const FunctionCallbackInfo<Value>& args) {
      Environment* env = Environment::GetCurrent(args);
      NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator();
      Local<ArrayBuffer> ab;
      // It can be a nullptr when running inside an isolate where we
      // do not own the ArrayBuffer allocator.
      if (allocator == nullptr) {
        // Create a dummy Uint32Array - the JS land can only toggle the C++ land
        // setting when the allocator uses our toggle. With this the toggle in JS
        // land results in no-ops.
        ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t));
      } else {
        uint32_t* zero_fill_field = allocator->zero_fill_field();
        std::unique_ptr<BackingStore> backing =
            ArrayBuffer::NewBackingStore(zero_fill_field,
                                         sizeof(*zero_fill_field),
                                         [](void*, size_t, void*) {},
                                         nullptr);
        ab = ArrayBuffer::New(env->isolate(), std::move(backing));
      }
    
      ab->SetPrivate(
          env->context(),
          env->untransferable_object_private_symbol(),
          True(env->isolate())).Check();
    
      args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1));
    }

    Allocate

    内存分配器的实现

    从代码实现可以看到如果 zero_fill_field 值为

    void* NodeArrayBufferAllocator::Allocate(size_t size) {
      void* ret;
      if (zero_fill_field_ || per_process::cli_options->zero_fill_all_buffers)
        ret = UncheckedCalloc(size);
      else
        ret = UncheckedMalloc(size);
      if (LIKELY(ret != nullptr))
        total_mem_usage_.fetch_add(size, std::memory_order_relaxed);
      return ret;
    }

    UncheckedCalloc UncheckedMalloc

    接着 Allocate 函数的内容

    关于 calloc 与 realloc 函数

    至此读到这里, 我们知道了 createUnsafeBuffer 创建未被初始化内存的完整实现, 在需要创建时设置 zero_fill_field 为 0 即假值即可, 同步创建成功再把 zero_fill_field 设置为 1 即真值就好了。

    inline T* UncheckedCalloc(size_t n) {
      if (n == 0) n = 1;
      MultiplyWithOverflowCheck(sizeof(T), n);
      return static_cast<T*>(calloc(n, sizeof(T)));
    }
    
    template <typename T>
    inline T* UncheckedMalloc(size_t n) {
      if (n == 0) n = 1;
      return UncheckedRealloc<T>(nullptr, n);
    }
    
    template <typename T>
    T* UncheckedRealloc(T* pointer, size_t n) {
      size_t full_size = MultiplyWithOverflowCheck(sizeof(T), n);
    
      if (full_size == 0) {
        free(pointer);
        return nullptr;
      }
    
      void* allocated = realloc(pointer, full_size);
    
      if (UNLIKELY(allocated == nullptr)) {
        // Tell V8 that memory is low and retry.
        LowMemoryNotification();
        allocated = realloc(pointer, full_size);
      }
    
      return static_cast<T*>(allocated);
    }

    其他实现

    通过 Uint8Array 如何写入读取 Int8Array 数据? 如通过 writeInt8 写入一个有符号的 -123 数据。

    const buf1 = Buffer.alloc(10);
    buf1.writeInt8(-123, 0)

    writeInt8, readInt8

    其实作为 Uint8Array 对应的 C 语言类型为 unsigned char, 可写入的范围为 0 到 255, 当写入一个有符号的值时如 -123, 其最高位符号位为 1, 其二进制的原码为 11111011, 最终存储在计算机中所有的数值都是用补码。所以其最终存储的补码为 10000101, 10 进制表示为 133。

    function writeInt8(value, offset = 0) {
      return writeU_Int8(this, value, offset, -0x80, 0x7f);
    }
    
    function writeU_Int8(buf, value, offset, min, max) {
      value = +value;
      // `checkInt()` can not be used here because it checks two entries.
      validateNumber(offset, 'offset');
      if (value > max || value < min) {
        throw new ERR_OUT_OF_RANGE('value', `>= ${min} and <= ${max}`, value);
      }
      if (buf[offset] === undefined)
        boundsError(offset, buf.length - 1);
    
      buf[offset] = value;
      return offset + 1;
    }
    
    function readInt8(offset = 0) {
      validateNumber(offset, 'offset');
      const val = this[offset];
      if (val === undefined)
        boundsError(offset, this.length - 1);
    
      return val | (val & 2 ** 7) * 0x1fffffe;
    }

    计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。

    通过 Uint8Array 如何写入读取 Uint16Array 数据?

    writeUInt16, readUInt16

    从下面的代码也是逐渐的看清了 Uint8Array 的实现, 如果写入 16 位的数组, 即会占用两个字节长度的 Uint8Array, 每个字节存储 8 位即可。

    function writeU_Int16BE(buf, value, offset, min, max) {
      value = +value;
      checkInt(value, min, max, buf, offset, 1);
    
      buf[offset++] = (value >>> 8);
      buf[offset++] = value;
      return offset;
    }
    
    function readUInt16BE(offset = 0) {
      validateNumber(offset, 'offset');
      const first = this[offset];
      const last = this[offset + 1];
      if (first === undefined || last === undefined)
        boundsError(offset, this.length - 2);
    
      return first * 2 ** 8 + last;
    }

    BE 指的是大端字节序, LE 指的是小端字节序, 使用何种方式都是可以的。小端字节序写用小端字节序读, 端字节序写就用大端字节序读, 读写规则不一致则会造成乱码, 更多可见 理解字节序

    writeFloatForwards, readFloatForwards

    对于 float32Array 的实现, 相当于直接使用了 float32Array

    const float32Array = new Float32Array(1);
    const uInt8Float32Array = new Uint8Array(float32Array.buffer);
    
    function writeFloatForwards(val, offset = 0) {
      val = +val;
      checkBounds(this, offset, 3);
    
      float32Array[0] = val;
      this[offset++] = uInt8Float32Array[0];
      this[offset++] = uInt8Float32Array[1];
      this[offset++] = uInt8Float32Array[2];
      this[offset++] = uInt8Float32Array[3];
      return offset;
    }
    
    function readFloatForwards(offset = 0) {
      validateNumber(offset, 'offset');
      const first = this[offset];
      const last = this[offset + 3];
      if (first === undefined || last === undefined)
        boundsError(offset, this.length - 4);
    
      uInt8Float32Array[0] = first;
      uInt8Float32Array[1] = this[++offset];
      uInt8Float32Array[2] = this[++offset];
      uInt8Float32Array[3] = last;
      return float32Array[0];
    }

    小结

    本文主要讲了 Node.js 中 Buffer 的实现, 相比直接使用 Uint8Array 等在性能安全以及使用上方便层度上做了一些改造, 有兴趣的同学可以扩展阅读 gRPC 中的 Protocol Buffers 的实现, 其遵循的是 Varints 编码 与 Zigzag 编码实现。

    更多编程相关知识,请访问:编程视频!!

    以上就是深入了解Nodejs中的buffer缓存区的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金--多小凯,如有侵犯,请联系admin@php.cn删除
    专题推荐:Nodejs buffer 缓存区
    上一篇:javascript如何实现html字符转实体 下一篇:浅谈Angular中@ViewChild的用法
    VIP会员

    相关文章推荐

    • 一文快速了解Node.js中的事件循环• 深入浅析Node.js异步编程中的callback(回调)• 浅谈nodejs执行bash脚本的几种方案• 这些Nodejs面试难题,你能回答上来吗?• 一文快速了解Nodejs中crypto模块的用法• 认识nvm工具,浅谈nvm如何切换Nodejs版本

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网