本篇文章帶大家了解NodeJs中的buffer快取區,介紹一下Node.js中Buffer的實現,一起來看看吧!
所涉及的知識點
先說一下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 有了處理二進位資料的能力, 其使用方式主要是分為如下幾步
通過ArrayBuffer 建構子, 建立長度為10 的記憶體區
透過Uint8Array 建構函式傳參數使其指向ArrayBuffer
#向操作數組一樣向第一個位元組寫入資料123
const buf1 = new ArrayBuffer(10); const x1 = new Uint8Array(buf1); x1[0] = 123;
#在Node.js 中也完全可以使用ArrayBuffer 相關的介面去處理二進位資料, 仔細看完ArrayBuffer 與Buffer 的文檔可以發現, Buffer 的進一步封裝能夠更簡單的上手與更好的性能, 接著讓我們去看看Buffer 的使用範例
透過alloc 方法建立長度為10 的記憶體區
透過writeUInt8 向第一個位元組寫入資料123
透過readUint8 讀取第一位元組的資料
const buf1 = Buffer.alloc(10); buf1.writeUInt8(123, 0) buf1.readUint8(0)
透過靜態方法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 操作記憶體, 如以下的範例
透過Uint8Array 建構函式建立長度為10 的記憶體區
向操作數組一樣向第一個位元組寫入資料123
const x1 = new Uint8Array(10); x1[0] = 123
那麼Node.js 中Buffer 僅透過Uint8Array 類別, 如何模擬實現下面所有的視圖類型的行為, 以及Buffer 又做了哪些其他的擴充了?
提供了 alloc, allocUnsafe, allocUnsafeSlow 3个方法去创建一个 Buffer 实例, 上面讲了 alloc 方法没有什么特别, 下面讲一下另外两种方法
与 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层面,以它作为单位单元进行内存的分配。
比起 allocUnsafe 从预先申请好的 allocPool 内存中切割出来的内存区, allocUnsafeSlow 是直接通过 createUnsafeBuffer 先创建的内存区域。从命名可知直接使用 Uint8Array 等都是 Slow 缓慢的。
Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) { assertSize(size); return createUnsafeBuffer(size); };
这个 Unsafe 不安全又是怎么回事了, 其实我们发现直接通过 Uint8Array 申请的内存都是填充了 0 数据的认为都是安全的, 那么 Node.js 又做了什么操作使其没有被填充数据了 ?
let zeroFill = getZeroFillToggle(); function createUnsafeBuffer(size) { zeroFill[0] = 0; try { return new FastBuffer(size); } finally { zeroFill[0] = 1; } }
那么我们只能去探究一下 zeroFill 在创建前后, 类似开关的操作的是如何实现这个功能
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)); }
内存分配器的实现
从代码实现可以看到如果 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; }
接着 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)
对写入的数值范围为 -128 到 127 进行了验证
直接进行赋值操作
其实作为 Uint8Array 对应的 C 语言类型为 unsigned char, 可写入的范围为 0 到 255, 当写入一个有符号的值时如 -123, 其最高位符号位为 1, 其二进制的原码为 11111011, 最终存储在计算机中所有的数值都是用补码。所以其最终存储的补码为 10000101, 10 进制表示为 133。
此时如果通过 readUInt8 去读取数据的话就会发现返回值为 133
如果通过 readInt8 去读取的话, 套用代码的实现 133 | (133 & 2 ** 7) * 0x1fffffe === -123 即满足要求
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 数据?
从下面的代码也是逐渐的看清了 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 指的是小端字节序, 使用何种方式都是可以的。小端字节序写用小端字节序读, 端字节序写就用大端字节序读, 读写规则不一致则会造成乱码, 更多可见 理解字节序。
对于 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中文網其他相關文章!