熟悉Buffer

前言

在引入 TypedArray 之前,JavaScript 語言沒有用於讀取或處理二進制數據流的機制。 Buffer 類是作爲 Node.js API 的一部分引入的,以允許對 TCP 流、文件等二進制數據進行操作。

本文所講內容都基於 Node 的 v10.x 版本。

Buffer 結構

Buffer 是一個類數組對象,主要用於操作字節。下面我們從模塊結構和對象結構的層面上來認識它。

模塊結構

Buffer 是一個典型的 JavaScript 與 C++ 結合的模塊,它將性能相關部分用 C++ 實現,將非性能相關的部分用 JavaScript 實現,如圖:

img-1

Buffer 所佔用的內存不是通過 V8 分配的,屬於堆外內存。

由於 Buffer 太過常見,Node 在進程啓動時就已經加載了它,並將其放在全局對象(global) 上。所以在使用 Buffer 時,無須通過 require() 即可直接使用。

Buffer 對象

Buffer 對象類似於數組,它的元素爲 16 進制的兩位數,即 0 到 255 的數值。示例代碼如下所示:

var str = '深入淺出node.js';
var buf = Buffer.from(str, 'utf8');
console.log(buf);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>

由上面的示例可見,不同編碼的字符串佔用的元素個數各不相同,上面代碼中的中文字在 UTF-8 編碼下佔用 3 個元素,字母和半角標點符號佔用 1 個元素。

Buffer 受 Array 類型的影響很大,可以訪問 length 屬性得到長度,也可以通過下標訪問元素, 在構造對象時也十分相似,代碼如下:

var buf = Buffer.alloc(10);
console.log(buf.length); // => 10
console.log(buf); // => <Buffer 00 00 00 00 00 00 00 00 00 00>

上述代碼分配了一個長 10 字節的 Buffer 對象。可以通過下標訪問剛初始化的 Buffer 的元素, 代碼如下:

console.log(buf[0]); // => 0

它的元素值是 0,這是 Buffer 分配時,沒提供初始化值的缺省值。

同樣,我們也可以通過下標對它進行賦值:

buf[0] = 255;
console.log(buf[0]); // => 255

值得注意的是,如果給元素賦值不是 0 到 255 的整數或是小數時會怎樣呢?示例代碼如下所示:

buf[0] = -100;
console.log(buf[0]); // 156
buf[1] = 300;
console.log(buf[1]); // 44
buf[2] = 3.1415;
console.log(buf[2]); // 3

給元素的賦值如果小於 0,就將該值逐次加 256,直到得到一個 0 到 255 之間的整數。如果得到的數值大於 255,就逐次減 256,直到得到 0~255 區間內的數值。如果是小數,捨棄小數部分,只保留整數部分。

Buffer 內存分配

Buffer 對象的內存分配不是在 V8 的堆內存中,而是在 Node 的 C++ 層面實現內存的申請的。因爲處理大量的字節數據不能採用需要一點內存就向操作系統申請一點內存的方式,這可能造成大量的內存申請的系統調用,對操作系統有一定壓力。爲此 Node 在內存的使用上應用的是在 C++ 層面申請內存、在 JavaScript 中分配內存的策略。

Buffer 的轉換

Buffer對象可以與字符串之間相互轉換。目前支持的字符串編碼類型有如下這幾種。

  • ASCII
  • UTF-8
  • UTF-16LE/UCS-2
  • Base64
  • Latin1/Binary
  • Hex

字符串轉 Buffer

字符串轉 Buffer 對象主要是通過 Buffer.from 函數完成的:

Buffer.from(string[, encoding]);

Buffer.from 函數轉換的 Buffer 對象,存儲的只能是一種編碼類型。encoding 參數不傳遞時,默認按UTF-8編碼進行轉碼和存儲。

一個 Buffer 對象可以存儲不同編碼類型的字符串轉碼的值,調用write()方法可以實現該目的,代碼如下:

buf.write(string[, offset[, length]][, encoding])

由於可以不斷寫入內容到 Buffer 對象中,並且每次寫入可以指定編碼,所以 Buffer 對象中可以存在多種編碼轉化後的內容。需要小心的是,每種編碼所用的字節長度不同,將 Buffer 反轉回字符串時需要謹慎處理。

Buffer 轉字符串

實現 Buffer 向字符串的轉換也十分簡單,Buffer 對象的 toString() 可以將 Buffer 對象轉換爲字符串,代碼如下:

buf.toString([encoding[, start[, end]]])

比較精巧的是,可以設置 encoding(默認爲UTF-8)、start、end 這3個參數實現整體或局部的轉換。如果 Buffer 對象由多種編碼寫入,就需要在局部指定不同的編碼,才能轉換回正常的編碼。

Buffer 不支持的編碼類型

目前比較遺憾的是,Node 的 Buffer 對象支持的編碼類型有限,只有少數的幾種編碼類型可以在字符串和 Buffer 之間轉換。爲此,Buffer 提供了一個 isEncoding() 函數來判斷編碼是否支持轉換:

Buffer.isEncoding(encoding)

將編碼類型作爲參數傳入上面的函數,如果支持轉換返回值爲 true,否則爲 false。很遺憾的是,在中國常用的 GBK、GB2312 和 BIG-5 編碼都不在支持的行列中。

對於不支持的編碼類型,可以藉助Node生態圈中的模塊完成轉換。iconv 和 iconv-lite 兩個模塊可以支持更多的編碼類型轉換,包括 Windows 125 系列、ISO-8859 系列、IBM/DOS 代碼頁系列、Macintosh 系列、KOI8 系列,以及 US-ASCII,也支持寬字節編碼 GBK 和 GB2312。

iconv-lite 採用純 JavaScript 實現,iconv 則通過 C++ 調用 libiconv 庫完成。前者比後者更輕量,無須編譯和處理環境依賴直接使用。在性能方面,由於轉碼都是耗用 CPU,在 V8 的高性能下,少了 C++ 到 JavaScript 的層次轉換,純 JavaScript 的性能比 C++ 實現得更好。

以下爲 iconv-lite 的示例代碼:

var iconv = require('iconv-lite');
// 字符串轉Buffer
var buf = iconv.encode('Sample input string', 'win1251');
console.log(buf);
// Buffer轉字符串
var str = iconv.decode(buf, 'win1251');
console.log(str); // => Sample input string

另外,iconv 和 iconv-lite 對無法轉換的內容進行降級處理時的方案不盡相同。iconv-lite 無法轉換的內容如果是多字節,會輸出 �;如果是單字節,則輸出 ?。iconv 則有三級降級策略,會嘗試翻譯無法轉換的內容,或者忽略這些內容。如果不設置忽略,iconv 對於無法轉換的內容將會得到 EILSEQ 異常。如下是 iconv 的示例代碼兼選項設置方式:

var Iconv = require('iconv').Iconv;

var iconv = new Iconv('UTF-8', 'ASCII');
try {
  iconv.convert('ça va'); // throws EILSEQ
} catch (err) {
  console.log(err);
}

var iconv = new Iconv('UTF-8', 'ASCII//IGNORE');
var str = iconv.convert('ça va'); // returns "a va"
console.log(str.toString());

var iconv = new Iconv('UTF-8', 'ASCII//TRANSLIT');
var str = iconv.convert('ça va'); // "ca va"
console.log(str.toString());

var iconv = new Iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE');
var str = iconv.convert('ça va が'); // "ca va "
console.log(str.toString());

Buffer 與性能

Buffer 在文件 I/O 和網絡 I/O 中運用廣泛,尤其在網絡傳輸中,它的性能舉足輕重。在應用中,我們通常會操作字符串,但一旦在網絡中傳輸,都需要轉換爲 Buffer,以進行二進制數據傳輸。 在 Web 應用中,字符串轉換到 Buffer 是時時刻刻發生的,提高字符串到 Buffer 的轉換效率,可以很大程度地提高網絡吞吐率。

在展開 Buffer 與網絡傳輸的關係之前,我們可以先來進行一次性能測試。下面的例子中構造了一個10 KB 大小的字符串。我們首先通過純字符串的方式向客戶端發送,代碼如下:

var http = require('http');
var helloworld = '';
for (var i = 0; i < 1024 * 10; i++) {
  helloworld += 'a';
}
// helloworld = Buffer.from(helloworld);
http
  .createServer(function(req, res) {
    res.writeHead(200);
    res.end(helloworld);
  })
  .listen(8001);

我們通過 ab 進行一次性能測試,發起 100 個併發客戶端:

ab -c 100 -t 2 http://127.0.0.1:8001/

得到的測試結果如下所示:

HTML transferred:       81848320 bytes
Requests per second:    3966.46 [#/sec] (mean)
Time per request:       25.211 [ms] (mean)
Time per request:       0.252 [ms] (mean, across all concurrent requests)
Transfer rate:          40226.90 [Kbytes/sec] received

測試的 QPS(每秒查詢次數)是3966.46,傳輸率爲每秒40226.90 KB。

接下來我們取消掉 helloworld = Buffer.from(helloworld); 前的註釋,使向客戶端輸出的是一個 Buffer 對象,無須在每次響應時進行轉換。再次進行性能測試的結果如下所示:

HTML transferred:       112220160 bytes
Requests per second:    5455.92 [#/sec] (mean)
Time per request:       18.329 [ms] (mean)
Time per request:       0.183 [ms] (mean, across all concurrent requests)
Transfer rate:          55190.45 [Kbytes/sec] received

QPS 的提升到5455.92,傳輸率爲每秒55190.45 KB,性能相比前面有較大提升。

通過預先轉換靜態內容爲 Buffer 對象,可以有效地減少 CPU 的重複使用,節省服務器資源。 在 Node 構建的 Web 應用中,可以選擇將頁面中的動態內容和靜態內容分離,靜態內容部分可以通過預先轉換爲 Buffer 的方式,使性能得到提升。由於文件自身是二進制數據,所以在不需要改變內容的場景下,儘量只讀取 Buffer,然後直接傳輸,不做額外的轉換,避免損耗。

參考文獻

  • 深入淺出 Node.js(樸靈)

  • https://nodejs.org/docs/latest-v10.x/api/buffer.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章