在Node、ES2015出現之前,前端工程師只需要進行一些簡單的字符串或DOM操作就可以滿足業務需要,所以對二進制數據是比較陌生。node出現以后,前端面對的技術場景發生了變化,可以深入到網絡傳輸、文件操作、圖片處理等領域,而這些操作都與二進制數據緊密相關。
Node里面的buffer,是一個二進制數據容器,數據結構類似與數組,數組里面的方法在buffer都存在(slice操作的結果不一樣)。下面就從源碼(v6.0版本)層面分析,揭開buffer操作的面紗。
1. buffer的基本使用
在Node 6.0以前,直接使用
new Buffer,但是這種方式存在兩個問題:
- 參數復雜: 內存分配,還是內存分配+內容寫入,需要根據參數來確定
- 安全隱患: 分配到的內存可能還存儲著舊數據,這樣就存在安全隱患
// 本來只想申請一塊內存,但是里面卻存在舊數據 const buf1 = new Buffer(10) // <Buffer 90 09 70 6b bf 7f 00 00 50 3a> // 不小心,舊數據就被讀取出來了 buf1.toString() // '?\tpk?\u0000\u0000P:'為了解決上述問題,Buffer提供了
Buffer.from、Buffer.alloc、Buffer.allocUnsafe、Buffer.allocUnsafeSlow四個方法來申請內存。// 申請10個字節的內存 const buf2 = Buffer.alloc(10) // <Buffer 00 00 00 00 00 00 00 00 00 00> // 默認情況下,用0進行填充 buf2.toString() //'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000' // 上述操作就相當于 const buf1 = new Buffer(10); buf.fill(0); buf.toString(); // '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'2. buffer的結構
buffer是一個典型的javascript與c++結合的模塊,其性能部分用c++實現,非性能部分用javascript來實現。
![]()
下面看看buffer模塊的內部結構:
exports.Buffer = Buffer; exports.SlowBuffer = SlowBuffer; exports.INSPECT_MAX_BYTES = 50; exports.kMaxLength = binding.kMaxLength;buffer模塊提供了4個接口:
- Buffer: 二進制數據容器類,node啟動時默認加載
- SlowBuffer: 同樣也是二進制數據容器類,不過直接進行內存申請
- INSPECT_MAX_BYTES: 限制
bufObject.inspect()輸出的長度- kMaxLength: 一次性內存分配的上限,大小為(2^31 - 1)
其中,由于Buffer經常使用,所以node在啟動的時候,就已經加載了Buffer,而其他三個,仍然需要使用
require('buffer').***。關于buffer的內存申請、填充、修改等涉及性能問題的操作,均通過c++里面的node_buffer.cc來實現:
// c++里面的node_buffer namespace node { bool zero_fill_all_buffers = false; namespace Buffer { ... } } NODE_MODULE_CONTEXT_AWARE_BUILTIN(buffer, node::Buffer::Initialize)3. 內存分配的策略
Node中Buffer內存分配太過常見,從系統性能考慮出發,Buffer采用了如下的管理策略。
![]()
3.1 Buffer.from
Buffer.from(value, ...)用于申請內存,并將內容寫入剛剛申請的內存中,value值是多樣的,Buffer是如何處理的呢?讓我們一起看看源碼:Buffer.from = function(value, encodingOrOffset, length) { if (typeof value === 'number') throw new TypeError('"value" argument must not be a number'); if (value instanceof ArrayBuffer) return fromArrayBuffer(value, encodingOrOffset, length); if (typeof value === 'string') return fromString(value, encodingOrOffset); return fromObject(value); };value可以分成三類:
- ArrayBuffer的實例: ArrayBuffer是ES2015里面引入的,用于在瀏覽器端直接操作二進制數據,這樣Node就與ES2015關聯起來,同時,新創建的Buffer與ArrayBuffer內存是共享的
- string: 該方法實現了將字符串轉變為Buffer
- Buffer/TypeArray/Array: 會進行值的copy
3.1.1 ArrayBuffer的實例
Node v6與時俱進,將瀏覽器、node中對二進制數據的操作關聯起來,同時二者會進行內存的共享。
var b = new ArrayBuffer(4); var v1 = new Uint8Array(b); var buf = Buffer.from(b) console.log('first, typeArray: ', v1) // first, typeArray: Uint8Array [ 0, 0, 0, 0 ] console.log('first, Buffer: ', buf) // first, Buffer: <Buffer 00 00 00 00> v1[0] = 12 console.log('second, typeArray: ', v1) // second, typeArray: Uint8Array [ 12, 0, 0, 0 ] console.log('second, Buffer: ', buf) // second, Buffer: <Buffer 0c 00 00 00>在上述操作中,對ArrayBuffer的操作,引起Buffer值的修改,說明二者在內存上是同享的,再從源碼層面了解下這個過程:
// buffer.js Buffer.from(arrayBuffer, ...)進入的分支: function fromArrayBuffer(obj, byteOffset, length) { byteOffset >>>= 0; if (typeof length === 'undefined') return binding.createFromArrayBuffer(obj, byteOffset); length >>>= 0; return binding.createFromArrayBuffer(obj, byteOffset, length); } // c++ 模塊中的node_buffer: void CreateFromArrayBuffer(const FunctionCallbackInfo<Value>& args) { ... Local<ArrayBuffer> ab = args[0].As<ArrayBuffer>(); ... Local<Uint8Array> ui = Uint8Array::New(ab, offset, max_length); ... args.GetReturnValue().Set(ui); }3.1.2 string
可以實現字符串與Buffer之間的轉換,同時考慮到操作的性能,采用了一些優化策略避免頻繁進行內存分配:
function fromString(string, encoding) { ... var length = byteLength(string, encoding); if (length === 0) return Buffer.alloc(0); // 當字符所需要的字節數大于4KB時: 直接進行內存分配 if (length >= (Buffer.poolSize >>> 1)) return binding.createFromString(string, encoding); // 當字符所需字節數小于4KB: 借助allocPool先申請、后分配的策略 if (length > (poolSize - poolOffset)) createPool(); var actual = allocPool.write(string, poolOffset, encoding); var b = allocPool.slice(poolOffset, poolOffset + actual); poolOffset += actual; alignPool(); return b; }a. 直接內存分配
當字符串所需要的字節大于4KB時,如何還從8KB的buffer pool中進行申請,那么就可能存在內存浪費,例如:
poolSize - poolOffset < 4KB: 這樣就要重新申請一個8KB的pool,剛才那個pool剩余空間就會被浪費掉
看看c++是如何進行內存分配的:
// c++ void CreateFromString(const FunctionCallbackInfo<Value>& args) { ... Local<Object> buf; if (New(args.GetIsolate(), args[0].As<String>(), enc).ToLocal(&buf)) args.GetReturnValue().Set(buf); }b. 借助于pool管理
用一個pool來管理頻繁的行為,在計算機中是非常常見的行為,例如http模塊中,關于tcp連接的建立,就設置了一個tcp pool。
function fromString(string, encoding) { ... // 當字符所需字節數小于4KB: 借助allocPool先申請、后分配的策略 // pool的空間不夠用,重新分配8kb的內存 if (length > (poolSize - poolOffset)) createPool(); // 在buffer pool中進行分配 var actual = allocPool.write(string, poolOffset, encoding); // 得到一個內存的視圖view, 特殊說明: slice不進行copy,僅僅創建view var b = allocPool.slice(poolOffset, poolOffset + actual); poolOffset += actual; // 校驗poolOffset是8的整數倍 alignPool(); return b; } // pool的申請 function createPool() { poolSize = Buffer.poolSize; allocPool = createBuffer(poolSize, true); poolOffset = 0; } // node加載的時候,就會創建個buffer pool createPool(); // 校驗poolOffset是8的整數倍 function alignPool() { // Ensure aligned slices if (poolOffset & 0x7) { poolOffset |= 0x7; poolOffset++; } }3.1.3 Buffer/TypeArray/Array
可用從一個現有的Buffer、TypeArray或Array中創建Buffer,內存不會共享,僅僅進行值的copy。
var buf1 = new Buffer([1,2,3,4,5]); var buf2 = new Buffer(buf1); console.log(buf1); // <Buffer 01 02 03 04 05> console.log(buf2); // <Buffer 01 02 03 04 05> buf1[0] = 16 console.log(buf1); // <Buffer 10 02 03 04 05> console.log(buf2); // <Buffer 01 02 03 04 05>上述示例就證明了buf1、buf2沒有進行內存的共享,僅僅是值的copy,再從源碼層面進行分析:
function fromObject(obj) { // 當obj為Buffer時 if (obj instanceof Buffer) { ... const b = allocate(obj.length); obj.copy(b, 0, 0, obj.length); return b; } // 當obj為TypeArray或Array時 if (obj) { if (obj.buffer instanceof ArrayBuffer || 'length' in obj) { ... return fromArrayLike(obj); } if (obj.type === 'Buffer' && Array.isArray(obj.data)) { return fromArrayLike(obj.data); } } throw new TypeError(kFromErrorMsg); } // 數組或類數組,逐個進行值的copy function fromArrayLike(obj) { const length = obj.length; const b = allocate(length); for (var i = 0; i < length; i++) b[i] = obj[i] & 255; return b; }3.2 Buffer.alloc
Buffer.alloc用于內存的分配,同時會對內存的舊數據進行覆蓋,避免安全隱患的產生。
Buffer.alloc = function(size, fill, encoding) { ... if (size <= 0) return createBuffer(size); if (fill !== undefined) { ... return typeof encoding === 'string' ? createBuffer(size, true).fill(fill, encoding) : createBuffer(size, true).fill(fill); } return createBuffer(size); }; function createBuffer(size, noZeroFill) { flags[kNoZeroFill] = noZeroFill ? 1 : 0; try { const ui8 = new Uint8Array(size); Object.setPrototypeOf(ui8, Buffer.prototype); return ui8; } finally { flags[kNoZeroFill] = 0; } }上述代碼有幾個需要注意的點:
3.2.1 先申請后填充
alloc先通過
createBuffer申請一塊內存,然后再進行填充,保證申請的內存全部用fill進行填充。var buf = Buffer.alloc(10, 11); console.log(buf); // <Buffer 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b>3.2.2 flags標示
flags用于標識默認的填充值是否為0,該值在javascript中設置,在c++中進行讀取。// js const binding = process.binding('buffer'); const bindingObj = {}; ... binding.setupBufferJS(Buffer.prototype, bindingObj); ... const flags = bindingObj.flags; const kNoZeroFill = 0; // c++ void SetupBufferJS(const FunctionCallbackInfo<Value>& args) { ... Local<Object> bObj = args[1].As<Object>(); ... bObj->Set(String::NewFromUtf8(env->isolate(), "flags"), Uint32Array::New(array_buffer, 0, fields_count)); }3.2.3 Uint8Array
Uint8Array是ES2015 TypeArray中的一種,可以在瀏覽器中創建二進制數據,這樣就把瀏覽器、Node連接起來。3.3 Buffer.allocUnSafe
Buffer.allocUnSafe與Buffer.alloc的區別在于,前者是從采用
allocate的策略,嘗試從buffer pool中申請內存,而buffer pool是不會進行默認值填充的,所以這種行為是不安全的。Buffer.allocUnsafe = function(size) { assertSize(size); return allocate(size); };3.4 Buffer.allocUnsafeSlow
Buffer.allocUnsafeSlow有兩個大特點: 直接通過c++進行內存分配;不會進行舊值填充。
Buffer.allocUnsafeSlow = function(size) { assertSize(size); return createBuffer(size, true); };4. 結語
字符串與Buffer之間存在較大的差距,同時二者又存在編碼關系。通過Node,前端工程師已經深入到網絡操作、文件操作等領域,對二進制數據的操作就顯得非常重要,因此理解Buffer的諸多細節十分必要。
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。