深入學習 Node.js Buffer

  • 2019 年 11 月 6 日
  • 筆記

友情提示:本文篇幅較長,可根據實際需要,進行選擇性閱讀。另外,對源碼感興趣的小夥伴,建議採用閱讀和調試相結合的方式,進行源碼學習。詳細的調試方式,請參考 Debugging Node.js Apps 文章。

預備知識

ArrayBuffer

ArrayBuffer 對象用來表示通用的、固定長度的原始二進制數據緩衝區。ArrayBuffer 不能直接操作,而是要通過類型數組對象DataView 對象來操作,它們會將緩衝區中的數據表示為特定的格式,並通過這些格式來讀寫緩衝區的內容。

ArrayBuffer 簡單說是一片內存,但是你不能(也不方便)直接用它。這就好比你在 C 裏面,malloc 一片內存出來,你也會把它轉換成 unsigned_int32 或者 int16 這些你需要的實際類型的數組/指針來用。 這就是JS里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是給 ArrayBuffer 提供了一個 「View」,MDN上的原話叫做 「Multiple views on the same data」,對它們進行下標讀寫,最終都會反應到它所建立在的 ArrayBuffer 之上。 來源 https://www.zhihu.com/question/30401979

語法

new ArrayBuffer(length)

  • 參數:length 表示要創建的 ArrayBuffer 的大小,單位為位元組。
  • 返回值:一個指定大小的 ArrayBuffer 對象,其內容被初始化為 0。
  • 異常:如果 length 大於 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或為負數,則拋出一個 RangeError 異常。
示例

下面的例子創建了一個 8 位元組的緩衝區,並使用一個 Int32Array 來引用它:

var buffer = new ArrayBuffer(8);  var view   = new Int32Array(buffer);

從 ECMAScript 2015 開始,ArrayBuffer 對象需要用 new 運算符創建。如果調用構造函數時沒有使用 new,將會拋出 TypeError 異常。

Unit8Array

Uint8Array 數組類型表示一個 8 位無符號整型數組,創建時內容被初始化為 0。創建完後,可以以對象的方式或使用數組下標索引的方式引用數組中的元素。

語法

Uint8Array(length);//創建初始化為0的,包含length個元素的無符號整型數組 Uint8Array(typedArray); Uint8Array(object); Uint8Array(buffer [, byteOffset [, length]]);

示例
// 來自長度  var uint8 = new Uint8Array(2);  uint8[0] = 42;  console.log(uint8[0]); // 42  console.log(uint8.length); // 2  console.log(uint8.BYTES_PER_ELEMENT); // 1    // 來自數組  var arr = new Uint8Array([21,31]);  console.log(arr[1]); // 31    // 來自另一個 TypedArray  var x = new Uint8Array([21, 31]);  var y = new Uint8Array(x);  console.log(y[0]); // 21    // 來自 ArrayBuffer  var buffer = new ArrayBuffer(8);  var z = new Uint8Array(buffer, 1, 4);

ArrayBuffer 和 TypedArray

ArrayBuffer 本身只是一個 0 和 1 存放在一行裏面的一個集合,ArrayBuffer 不知道第一個和第二個元素在數組中該如何分配。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

為了能提供上下文,我們需要將其封裝在一個叫做 View 的東西裏面。這些在數據上的 View 可以被添加進確定類型的數組,而且我們有很多種確定類型的數據可以使用。

例如,你可以使用一個 Int8 的確定類型數組來分離存放 8 位二進制位元組。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

或者你可以使用一個無符號的 Int16 數組來分離存放 16 位二進制位元組,這樣如果是一個無符號的整數也能處理。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

你甚至可以在相同基礎的 Buffer 上使用不同的 View,同樣的操作不同的 View 會給你不同的結果。

比如,如果我們在這個 ArrayBuffer 中從 Int8 View 里獲取了元素 0 和 1,在 Uint16 View 中元素 0 會返回給我們不同的值,儘管它們包含的是完全相同的二進制位元組。

(圖片來源 —— A cartoon intro to ArrayBuffers and SharedArrayBuffers

在這種方式中,ArrayBuffer 基本上扮演了一個原生內存的角色,它模擬了像 C 語言才有的那種直接訪問內存的方式。你可能想知道為什麼我們不讓程序直接訪問內存,而是添加了這種抽象層,因為直接訪問內存將導致一些安全漏洞

Node.js Buffer

在 ECMAScript 2015 (ES6) 引入 TypedArray 之前,JavaScript 語言沒有讀取或操作二進制數據流的機制。Buffer 類被引入作為 Node.js API 的一部分,使其可以在 TCP 流或文件系統操作等場景中處理二進制數據流。

TypedArray 現已被添加進 ES6 中,Buffer 類以一種更優化、更適合 Node.js 用例的方式實現了 Uint8Array API。

Buffer 類的實例類似於整數數組,但 Buffer 的大小是固定的、且在 V8 堆外分配物理內存。 Buffer 的大小在被創建時確定,且無法調整。

Buffer 基本使用

// 創建一個長度為 10、且用 0 填充的 Buffer。  const buf1 = Buffer.alloc(10);    // 創建一個長度為 10、且用 0x1 填充的 Buffer。  const buf2 = Buffer.alloc(10, 1);    // 創建一個長度為 10、且未初始化的 Buffer。  // 這個方法比調用 Buffer.alloc() 更快,  // 但返回的 Buffer 實例可能包含舊數據,  // 因此需要使用 fill() 或 write() 重寫。  const buf3 = Buffer.allocUnsafe(10);    // 創建一個包含 [0x1, 0x2, 0x3] 的 Buffer。  const buf4 = Buffer.from([1, 2, 3]);    // 創建一個包含 UTF-8 位元組 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer。  const buf5 = Buffer.from('tést');    // 創建一個包含 Latin-1 位元組 [0x74, 0xe9, 0x73, 0x74] 的 Buffer。  const buf6 = Buffer.from('tést', 'latin1');

Buffer.from(), Buffer.alloc(), and Buffer.allocUnsafe()

在 Node.js v6 之前的版本中,Buffer 實例是通過 Buffer 構造函數創建的,它根據提供的參數返回不同的 Buffer:

  • 傳一個數值作為第一個參數給 Buffer()(如 new Buffer(10)),則分配一個指定大小的新建的 Buffer 對象。 在 Node.js 8.0.0 之前,分配給這種 Buffer 實例的內存是沒有初始化的,且可能包含敏感數據。 這種 Buffer 實例隨後必須被初始化,可以使用 buf.fill(0) 或寫滿這個 Buffer。 雖然這種行為是為了提高性能而有意為之的,但開發經驗表明,創建一個快速但未初始化的 Buffer 與創建一個慢點但更安全的 Buffer 之間需要有更明確的區分。從 Node.js 8.0.0 開始, Buffer(num)new Buffer(num) 將返回一個初始化內存之後的 Buffer
  • 傳一個字符串、數組、或 Buffer 作為第一個參數,則將所傳對象的數據拷貝到 Buffer 中。
  • 傳入一個 ArrayBuffer,則返回一個與給定的 ArrayBuffer 共享所分配內存的 Buffer

為了使 Buffer 實例的創建更可靠、更不容易出錯,各種 new Buffer() 構造函數已被廢棄,並由 Buffer.from()Buffer.alloc()、和 Buffer.allocUnsafe() 方法替代。

為什麼 Buffer.allocUnsafe() 和 Buffer.allocUnsafeSlow() 不安全

當調用 Buffer.allocUnsafe()Buffer.allocUnsafeSlow() 時,被分配的內存段是未初始化的(沒有用 0 填充)。 雖然這樣的設計使得內存的分配非常快,但已分配的內存段可能包含潛在的敏感舊數據。 使用通過 Buffer.allocUnsafe() 創建的沒有被完全重寫內存的 Buffer ,在 Buffer內存可讀的情況下,可能泄露它的舊數據。

雖然使用 Buffer.allocUnsafe() 有明顯的性能優勢,但必須額外小心,以避免給應用程序引入安全漏洞。

Buffer 與字符編碼

Buffer 實例一般用於表示編碼字符的序列,比如 UTF-8 、 UCS2 、 Base64 、或十六進制編碼的數據。 通過使用顯式的字符編碼,就可以在 Buffer 實例與普通的 JavaScript 字符串之間進行相互轉換。

示例
const buf = Buffer.from('hello world', 'ascii');    // 輸出 68656c6c6f20776f726c64  console.log(buf.toString('hex'));    // 輸出 aGVsbG8gd29ybGQ=  console.log(buf.toString('base64'));

Node.js 目前支持的字符編碼包括:

  • 'ascii' – 僅支持 7 位 ASCII 數據。如果設置去掉高位的話,這種編碼是非常快的。
  • 'utf8' – 多位元組編碼的 Unicode 字符。許多網頁和其他文檔格式都使用 UTF-8 。
  • 'utf16le' – 2 或 4 個位元組,小位元組序編碼的 Unicode 字符。支持代理對(U+10000 至 U+10FFFF)。
  • 'ucs2''utf16le' 的別名。
  • 'base64' – Base64 編碼。當從字符串創建 Buffer 時,按照 RFC4648 第 5 章的規定,這種編碼也將正確地接受 「URL 與文件名安全字母表」。
  • 'latin1' – 一種把 Buffer 編碼成一位元組編碼的字符串的方式(由 IANA 定義在 RFC1345 第 63 頁,用作 Latin-1 補充塊與 C0/C1 控制碼)。
  • 'binary''latin1' 的別名。
  • 'hex' – 將每個位元組編碼為兩個十六進制字符。

Buffer 與 TypedArray

Buffer 實例也是 Uint8Array 實例。 但是與 ECMAScript 2015 中的 TypedArray 規範還是有些微妙的不同。 例如,當 ArrayBuffer#slice() 創建一個切片的副本時,Buffer#slice() 的實現是在現有的 Buffer 上不經過拷貝直接進行創建,這也使得 Buffer#slice() 更高效。

遵循以下注意事項,也可以從一個 Buffer 創建一個新的 TypedArray 實例:

  1. Buffer 對象的內存是拷貝到 TypedArray 的,而不是共享的。
  2. Buffer 對象的內存是被解析為一個明確元素的數組,而不是一個目標類型的位元組數組。 也就是說,new Uint32Array(Buffer.from([1, 2, 3, 4])) 會創建一個包含 [1, 2, 3, 4] 四個元素的 Uint32Array,而不是一個只包含一個元素 [0x1020304][0x4030201]Uint32Array

也可以通過 TypeArray 對象的 .buffer 屬性創建一個新建的且與 TypedArray 實例共享同一分配內存的 Buffer

Buffer 內存管理

在介紹 Buffer 內存管理之前,我們要先來介紹一下 Buffer 內部的 8K 內存池。

8K 內存池

在 Node.js 應用程序啟動時,為了方便地、高效地使用 Buffer,會創建一個大小為 8K 的內存池。

Buffer.poolSize = 8 * 1024; // 8K  var poolSize, poolOffset, allocPool;    // 創建內存池  function createPool() {    poolSize = Buffer.poolSize;    allocPool = createUnsafeArrayBuffer(poolSize);    poolOffset = 0;  }    createPool();

在 createPool() 函數中,通過調用 createUnsafeArrayBuffer() 函數來創建 poolSize(即8K)的 ArrayBuffer 對象。createUnsafeArrayBuffer() 函數的實現如下:

function createUnsafeArrayBuffer(size) {    zeroFill[0] = 0;    try {      return new ArrayBuffer(size); // 創建指定size大小的ArrayBuffer對象,其內容被初始化為0。    } finally {      zeroFill[0] = 1;    }  }

這裡你只需知道 Node.js 應用程序啟動時,內部有個 8K 的內存池即可。那接下來我們要介紹哪個對象呢?在前面的預備知識部分,我們簡單介紹了 ArrayBuffer 和 Unit8Array 相關的基礎知識,而 ArrayBuffer 的應用在 8K 的內存池部分的已經介紹過了。那接下來當然要輪到 Unit8Array 了,我們再來回顧一下它的語法:

Uint8Array(length);  Uint8Array(typedArray);  Uint8Array(object);  Uint8Array(buffer [, byteOffset [, length]]);

其實除了 Buffer 類外,還有一個 FastBuffer 類,該類的聲明如下:

class FastBuffer extends Uint8Array {    constructor(arg1, arg2, arg3) {      super(arg1, arg2, arg3);    }  }

是不是知道 Uint8Array 用在哪裡了,在 FastBuffer 類的構造函數中,通過調用 Uint8Array(buffer [, byteOffset [, length]]) 來創建 Uint8Array 對象。

那麼現在問題來了,FastBuffer 有什麼用?它和 Buffer 類有什麼關係?帶着這兩個問題,我們先來一起分析下面的簡單示例:

const buf = Buffer.from('semlinker');  console.log(buf);

以上代碼運行後輸出的結果如下:

<Buffer 73 65 6d 6c 69 6e 6b 65 72>

什麼鬼,竟然輸出了一串數字,是誰偷走了我的字母?經過好心人引薦,我找到私家偵探毛利小五郎,打算重金請他幫我調查字母丟失案,期間在偵探社遇到了一個名叫柯南的小帥哥,他告訴我 「真相只有一個,請從源碼找答案」。聽完這句話,我茅塞頓開,從此踏上了漫漫的源碼求解之路。

/**   * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError   * if value is a number.   * Buffer.from(str[, encoding])   * Buffer.from(array)   * Buffer.from(buffer)   * Buffer.from(arrayBuffer[, byteOffset[, length]])   **/  Buffer.from = function from(value, encodingOrOffset, length) {    if (typeof value === "string") return fromString(value, encodingOrOffset);    // 處理其它數據類型,省略異常處理等其它代碼    if (isAnyArrayBuffer(value))      return fromArrayBuffer(value, encodingOrOffset, length);    var b = fromObject(value);  };

可以看出 Buffer.from() 工廠函數,支持基於多種數據類型(string、array、buffer 等)創建 Buffer 對象。對於字符串類型的數據,內部調用 fromString(value, encodingOrOffset) 方法來創建 Buffer 對象。

是時候來會一會 fromString() 方法了,它內部實現如下:

function fromString(string, encoding) {    var length;    if (typeof encoding !== "string" || encoding.length === 0) {      if (string.length === 0) return new FastBuffer();      // 若未設置編碼,則默認使用utf8編碼。      encoding = "utf8";      // 使用 buffer binding 提供的方法計算string的長度      length = byteLengthUtf8(string);    } else {  	// 基於指定的 encoding 計算string的長度      length = byteLength(string, encoding, true);      if (length === -1)        throw new errors.TypeError("ERR_UNKNOWN_ENCODING", encoding);      if (string.length === 0) return new FastBuffer();    }      // 當字符串所需位元組數大於4KB,則直接進行內存分配    if (length >= Buffer.poolSize >>> 1)      // 使用 buffer binding 提供的方法,創建buffer對象      return createFromString(string, encoding);      // 當剩餘的空間小於所需的位元組長度,則先重新申請8K內存    if (length > poolSize - poolOffset)      // allocPool = createUnsafeArrayBuffer(8K); poolOffset = 0;      createPool();    // 創建 FastBuffer 對象,並寫入數據。    var b = new FastBuffer(allocPool, poolOffset, length);    const actual = b.write(string, encoding);    if (actual !== length) {      // byteLength() may overestimate. That's a rare case, though.      b = new FastBuffer(allocPool, poolOffset, actual);    }    // 更新pool的偏移,並執行位元組對齊    poolOffset += actual;    alignPool();    return b;  }

現在我們來梳理一下幾個注意項:

  • 當未設置編碼的時候,默認使用 utf8 編碼;
  • 當字符串所需位元組數大於4KB,則直接進行內存分配;
  • 當字符串所需位元組數小於4KB,但超過預分配的 8K 內存池的剩餘空間,則重新申請 8K 的內存池;
  • 調用 new FastBuffer(allocPool, poolOffset, length) 創建 FastBuffer 對象,進行數據存儲,數據成功保存後,會進行長度校驗、更新 poolOffset 偏移量和位元組對齊等操作。

相信很多小夥伴跟我一樣,第一次聽到位元組對齊這個概念,這裡我們先不展開,後面再來簡單介紹它。這時,字母丟失案漸漸有了一點眉目,原來我們字符串中的字符,使用默認的 utf8 編碼後才保存到內存中。現在是時候該介紹一下 ascii、unicode 和 utf8 編碼了。

ascii、unicode 和 utf8

ascii 編碼

ASCII(American Standard Code for Information Interchange,美國信息交換標準代碼)是基於拉丁字母的一套電腦編碼系統,主要用於顯示現代英語和其他西歐語言。它是現今最通用的單位元組編碼系統,並等同於國際標準ISO/IEC 646。—— 百度百科

ASCII 碼使用指定的 7 位或 8 位二進制數組合來表示 128 或 256 種可能的字符。標準 ASCII 碼也叫基礎ASCII 碼,使用7 位二進制數(剩下的1位二進制為0)來表示所有的大寫和小寫字母,數字 0 到 9、標點符號, 以及在美式英語中使用的特殊控制字符

  • 0~31及127(共33個)是控制字符或通信專用字符(其餘為可顯示字符),如控制符:LF(換行)、CR(回車)、FF(換頁)、DEL(刪除)等。
  • 32~126 (共95個) 是字符 (32是空格),其中 48~57 為 0 到 9 十個阿拉伯數字。
  • 65~90 為 26 個大寫英文字母,97~122 號為 26 個小寫英文字母,其餘為一些標點符號、運算符號等。

後 128 個稱為擴展ASCII碼。許多基於x86的系統都支持使用擴展 ASCII。擴展 ASCII 碼允許將每個字符的第 8 位用於確定附加的 128 個特殊符號字符、外來語字母和圖形符號。

小結

在計算機內部,位元組是最小的單位,一位元組為 8 位,每一位可能的值為 0 或 1。標準 ASCII 碼使用指定的 7 位二進制數來表示 128 種可能的字符。後 128 個稱為擴展 ASCII 碼,它允許將每個字符的第 8 位用於確定附加的 128 個特殊符號字符、外來語字母和圖形符號。

unicode 編碼

全世界那麼多語言文字,僅使用 ascii 編碼肯定遠遠不夠。這時,我們就得來介紹一下 unicode 編碼。

Unicode(統一碼、萬國碼、單一碼)是計算機科學領域裏的一項業界標準,包括字符集、編碼方案等。Unicode 是為了解決傳統的字符編碼方案的局限而產生的,它為每種語言中的每個字符設定了統一併且唯一的二進制編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求。—— 百度百科

Unicode 也是一種字符編碼方法,不過它是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode的全稱是 「Universal Multiple-Octet Coded Character Set」,簡稱為 UCS。UCS 可以看作是 「Unicode Character Set」 的縮寫。

不過 UCS 只是規定如何編碼,並沒有規定如何傳輸、保存這個編碼。例如漢字 「超」 字的 UCS 編碼是 8d85,我們可以用 4 個 ascii 碼來傳輸、保存這個編碼;也可以用 utf8 編碼:3 個連續的位元組 E8 B6 85 來表示它。關鍵在於通信雙方都要認可。

小結

Unicode 是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode 的學名是 Universal Multiple-Octet Coded Character Set,簡稱為 UCS。UCS 只是規定如何編碼,並沒有規定如何傳輸、保存這個編碼。

utf8 編碼

前面已經介紹過了漢字 「超」 字的 UCS 編碼是 8d85,而對應的 utf8 編碼為 E8 B6 85。接下來我們來了解一下 utf8 編碼。

UTF-8(8-bit Unicode Transformation Format)是一種針對 Unicode 的可變長度字符編碼,又稱萬國碼。由Ken Thompson於1992年創建。現在已經標準化為RFC 3629。UTF-8用1到6個位元組編碼Unicode字符。用在網頁上可以統一頁面顯示中文簡體繁體及其它語言(如英文,日文,韓文)。 —— 百度百科

通過百度百科的定義,我們知道 UTF 的全稱為 「Unicode Transformation Format」。UTF-8 是一種針對 Unicode 的可變長度字符編碼。UTF-8 就是以 8 位為單元對 UCS 進行編碼,而 UTF-8 不使用大尾序和小尾序的形式,每個使用 UTF-8 存儲的字符,除了第一個位元組外,其餘位元組的頭兩個比特都是以 「10」 開始,使文字處理器能夠較快地找出每個字符的開始位置。

Unicode 和 UTF-8 之間的轉換關係表 ( x 字符表示碼點佔據的位 )

碼點的位數

碼點起值

碼點終值

位元組序列

Byte 1

Byte 2

Byte 3

Byte 4

Byte 5

Byte 6

7

U+0000

U+007F

1

0xxxxxxx

11

U+0080

U+07FF

2

110xxxxx

10xxxxxx

16

U+0800

U+FFFF

3

1110xxxx

10xxxxxx

10xxxxxx

21

U+10000

U+1FFFFF

4

11110xxx

10xxxxxx

10xxxxxx

10xxxxxx

26

U+200000

U+3FFFFFF

5

111110xx

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

31

U+4000000

U+7FFFFFFF

6

1111110x

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

  • 在 ASCII 碼的範圍,用一個位元組表示,超出 ASCII 碼的範圍就用多個位元組表示,這就形成了我們上面看到的 UTF-8的表示方法,這樣的好處是當 UNICODE 文件中只有 ASCII 碼時,存儲的文件都為一個位元組,所以就是普通的ASCII 文件無異,讀取的時候也是如此,所以能與以前的 ASCII 文件兼容。
  • 大於 ASCII 碼的,就會由上面的第一位元組的前幾位表示該 unicode 字符的長度,即在多位元組串中,第一個位元組的開頭 「1」 的數目就是整個串中位元組的數目。比如在(U+0080 – U+07FF)碼點範圍的第一位元組為 110xxxxx ,該位元組高位有連續兩個 1,因此表示在(U+0080 – U+07FF)範圍內的 unicode 碼值,使用 utf8 編碼後,佔用兩個位元組。
小結

UTF 的全稱為 「Unicode Transformation Format」,UTF-8 就是以 8 位為單元對 UCS 進行編碼,它是一種針對 Unicode 的可變長度字符編碼。對應的 UCS 碼值,如果在 ASCII 碼的範圍,用一個位元組表示,超出 ASCII 碼的範圍就用多個位元組表示。這樣的好處是為了節省存儲空間,提高網絡傳輸的效率。

了解完 ascii、unicode 和 utf8 相關的知識,各位小夥伴是不是對字母丟失案已經有了大概的結論。

接下來我們再來回顧一下字母丟失案

const buf = Buffer.from('semlinker');  console.log(buf); // <Buffer 73 65 6d 6c 69 6e 6b 65 72>  console.log(buf.length); // 9

由於調用 from() 方法時,我們沒有設定編碼,所以默認使用 utf8 編碼。在 ascii/unicode 編碼中,65~90 為 26 個大寫英文字母,97~122 號為 26 個小寫英文字母。它們的碼點在 (U+0000 – U+007F)範圍內,因此根據

「Unicode 和 UTF-8 之間的轉換關係表」 我們可以知道對於大小寫英文字母來說,它們的 ascii/utf8 編碼值是一樣的,此時字母丟失案已經告破了。難道這樣就結束了,其實我想說這只是告一段落。

Buffer 中文處理

字母丟失案中我們已經知道可以通過 Buffer.from('semlinker') 來創建 Buffer 對象,然後利用 length 屬性來獲取 Buffer 的長度,但如果運行以下代碼:

const buf = Buffer.from('超');  console.log(buf);  console.log(buf.length);

它的輸出結果是什麼?估計仔細看過前面 「ascii、unicode 和 utf8」 章節的小夥伴,已經知道輸出結果為 <Buffer e8 b6 85>3 了。前面已經介紹過 「Unicode 和 UTF-8 之間的轉換關係表」,接下來我們利用該關係表,來手動進行 utf8 編碼。

漢字 「超」 字的 UCS 編碼是 8d85,處於的碼點範圍為 (U+0800 – U+FFFF),所以使用以下模板:

1110xxxx 10xxxxxx 10xxxxxx

接下來列出 8d85 每一位對應的二進制值,具體值如下:

8 —— 1000  d —— 1101  8 —— 1000  5 —— 0101

然後從後向前按照 5 – 8 – d – 8 的順序依次進行位填充,多出的位補 0,最終填充後的結果如下:

11101000 10110110 10000101

以上二進制格式對應的十六進制表示為 e8 b6 85。相信到這裡,你已經對 Buffer 中文處理有了一個大致的了解。那麼現在問題又來了,我們應該如何讀取保存到 Buffer 對象中的數據,其實我們可以通過下標來訪問 Buffer 中保存的數據,具體方式如下:

const buf = Buffer.from('semlinker');  console.log(buf[0]); // 十進制:115 十六進制:0x73  console.log(buf[1]); // 十進制:101 十六進制:0x65

雖然我們已經可以訪問到每個位元組的數據,但如果我們想獲取原始的 「semlinker」 字符串呢?Buffer 類也為我們考慮到了這個需求,為了提供了 toString() 方法,該方法的簽名如下:

Buffer.prototype.toString = function toString(encoding, start, end) { }

所以當我們需要獲取原始的 「semlinker」 字符串時,我們可以使用 buf.toString('utf8') 來實現解碼操作。需要注意的是目前 Node.js 支持的字符編碼包括:ascii、utf8、utf16le (別名 ucs2)、base64、latin1 (別名 binary) 和 hex。

不知道小夥伴們有沒有發現,Buffer 對象與 Array 對象有很多相同之處,比如它們都有 length 屬性、from() 方法、toString() 方法和 slice() 方法等。但 Buffer 對象的 slice() 方法與 Array 對象的 slice() 方法還是有區別的。

Buffer slice() vs Array slice()

Array slice()

slice() 方法返回一個從開始到結束(不包括結束)選擇的數組的一部分淺拷貝到一個新數組對象,且原始數組不會被修改

示例
var animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];  console.log(animals.slice(2)); // ["camel", "duck", "elephant"]  console.log(animals); //  ["ant", "bison", "camel", "duck", "elephant"]

Buffer slice()

slice() 返回一個指向相同原始內存的新建的 Buffer,但做了偏移且通過 startend 索引進行裁剪。

注意,修改這個新建的 Buffer 切片,也會同時修改原始的 Buffer 的內存,因為這兩個對象所分配的內存是重疊的。

示例
const buf = Buffer.from('semlinker');  const buf1 = buf.slice(0, 3);  buf1[0] = 97;  console.log(buf); // <Buffer 61 65 6d 6c 69 6e 6b 65 72>  console.log(buf1); // <Buffer 61 65 6d>  console.log(buf.toString('utf8')); // aemlinker

通過觀察 Array slice() 示例和 Buffer slice() 示例的輸出結果,我們更加直觀地了解它們之間的差異。

Buffer 對象的 slice() 方法具體實現如下:

Buffer.prototype.slice = function slice(start, end) {    const srcLength = this.length;    start = adjustOffset(start, srcLength);    end = end !== undefined ? adjustOffset(end, srcLength) : srcLength;    const newLength = end > start ? end - start : 0;    // 與原始的Buffer對象,共用內存。    return new FastBuffer(this.buffer, this.byteOffset + start, newLength);  };

最後我們再來簡單介紹一下位元組對齊的概念。

位元組對齊

所謂的位元組對齊,就是各種類型的數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這個就是對齊。我們經常聽說的對齊在 N 上,它的含義就是數據的存放起始地址 %N== 0。首先還是讓我們來看一下,為什麼要進行位元組對齊吧。

各個硬件平台對存儲空間的處理上有很大的不同。一些平台對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的 CPU,諸如 SPARC 在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構上必須編程必須保證位元組對齊,而有些平台對於沒有進行對齊的數據進行存取時會產生效率的下降

讓我們來以 x86 為例看一下如果在不進行對齊的情況下,會帶來什麼樣子的效率低下問題,看下面的數據結構聲明:

struct A {    char c;  // 字符佔一個位元組    int i; // 整型佔四個位元組  };  struct A a;

假設變量 a 存放在內存中的起始地址為 0x00,那麼其成員變量 c 的起始地址為 0x00,成員變量 i 的起始地址為0x01,變量 a 一共佔用了 5 個位元組。當 CPU 要對成員變量 c 進行訪問時,只需要一個讀周期即可。

然而如果要對成員變量 i 進行訪問,那麼情況就變得有點複雜了,首先 CPU 用了一個讀周期,從 0x00 處讀取了 4 個位元組(注意由於是 32 位架構),然後將 0x01-0x03 的 3 個位元組暫存,接着又花費了一個讀周期讀取了從 0x04 – 0x07 的 4 位元組數據,將 0x04 這個位元組與剛剛暫存的 3 個位元組進行拼接從而讀取到成員變量 i 的值。

為了讀取這個成員變量 i,CPU 花費了整整 2 個讀周期。試想一下,如果數據成員 i 的起始地址被放在了 0x04 處,那麼讀取其所花費的周期就變成了 1,顯然引入位元組對齊可以避免讀取效率的下降,但這同時也浪費了 3 個位元組的空間 (0x01-0x03)。

了解完位元組對齊的概念和使用位元組對齊的原因,最後我們來看一下 Buffer.js 文件中的實現位元組對齊的 alignPool() 函數:

/**   * 如果不按照平台要求對數據存放進行對齊,會帶來存取效率上的損失。比如32位的   * Intel處理器通過總線訪問內存數據。每個總線周期從偶地址開始訪問32位內存數   * 據,內存數據以位元組為單位存放。如果一個32位的數據沒有存放在4位元組整除的內   * 存地址處,那麼處理器就需要2個總線周期對其進行訪問,顯然訪問效率下降很多。   */  function alignPool() {    // Ensure aligned slices    // 後四位:0001|0010|0011|0100|0101|0110|0111    if (poolOffset & 0x7) {      poolOffset |= 0x7;      poolOffset++;    }  }

總結

為了深入學習 Node.js 中的 Buffer 對象,本文介紹了 ArrayBuffer、Uint8Array、常用編碼和內存對齊等相關知識。然後通過簡單的示例,介紹了 Buffer.from() 工廠函數,接着我們以字符串 'semlinker' 為輸入參數,詳細分析了 buffer.js 文件中 fromString() 函數。最後,我們使用簡單示例介紹了 Array 對象 slice() 方法與 Buffer 對象 slice() 方法的區別。

參考資源