深入學習 Node.js Buffer
友情提示:本文篇幅較長,可根據實際需要,進行選擇性閱讀。另外,對原始碼感興趣的小夥伴,建議採用閱讀和除錯相結合的方式,進行原始碼學習。詳細的除錯方式,請參考 ofollow,noindex">Debugging Node.js Apps 文章。
預備知識
ArrayBuffer
ArrayBuffer 物件用來表示 通用的、固定長度的 原始二進位制資料緩衝區。 ArrayBuffer 不能直接操作,而是要通過 JavaScript/Reference/Global_Objects/TypedArray" target="_blank" rel="nofollow,noindex">型別陣列物件 或 JavaScript/Reference/Global_Objects/DataView" target="_blank" rel="nofollow,noindex"> 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
物件需要用 Operators/new" target="_blank" rel="nofollow,noindex"> 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
例項:
-
Buffer
物件的記憶體是拷貝到TypedArray
的,而不是共享的。 -
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; }
現在我們來梳理一下幾個注意項:
new FastBuffer(allocPool, poolOffset, length)
相信很多小夥伴跟我一樣,第一次聽到位元組對齊這個概念,這裡我們先不展開,後面再來簡單介紹它。這時,字母丟失案漸漸有了一點眉目,原來我們字串中的字元,使用預設的 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,最終填充後的結果如下:
以上二進位制格式對應的十六進位制表示為 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
,但做了偏移且通過 start
和 end
索引進行裁剪。
注意,修改這個新建的 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++; } }