Node源碼解析 -- buffer

博客原地址: 點擊這裏javascript

在Node、ES2015出現以前,前端工程師只須要進行一些簡單的字符串或DOM操做就能夠知足業務須要,因此對二進制數據是比較陌生。node出現之後,前端面對的技術場景發生了變化,能夠深刻到網絡傳輸、文件操做、圖片處理等領域,而這些操做都與二進制數據緊密相關。html

Node裏面的buffer,是一個二進制數據容器,數據結構相似與數組,數組裏面的方法在buffer都存在(slice操做的結果不同)。下面就從源碼(v6.0版本)層面分析,揭開buffer操做的面紗。前端

1. buffer的基本使用

在Node 6.0之前,直接使用new Buffer,可是這種方式存在兩個問題:java

  • 參數複雜: 內存分配,仍是內存分配+內容寫入,須要根據參數來肯定node

  • 安全隱患: 分配到的內存可能還存儲着舊數據,這樣就存在安全隱患c++

// 原本只想申請一塊內存,可是裏面卻存在舊數據
const buf1 = new Buffer(10) // <Buffer 90 09 70 6b bf 7f 00 00 50 3a>
// 不當心,舊數據就被讀取出來了
buf1.toString()  // '�\tpk�\u0000\u0000P:'

爲了解決上述問題,Buffer提供了Buffer.fromBuffer.allocBuffer.allocUnsafeBuffer.allocUnsafeSlow四個方法來申請內存。git

// 申請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來實現。github

圖片描述

下面看看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>

上述示例就證實了buf一、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的諸多細節十分必要。

相關文章
相關標籤/搜索