Node.js 中的緩衝區(Buffer)到底是什麼?

圖片描述

多數人都擁有本身不瞭解的能力和機會,都有可能作到不曾夢想的事情。 ——戴爾·卡耐基html

從前端轉入 Node.js 的童鞋對這一部份內容會比較陌生,由於在前端中一些簡單的字符串操做已經知足基本的業務需求,有時可能也會以爲 Buffer、Stream 這些會很神祕。回到服務端,若是你不想只作一名普通的 Node.js 開發工程師,你應該深刻去學習一下 Buffer 揭開這一層神祕的面紗,同時也會讓你對 Node.js 的理解提高一個水平。前端

做者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公衆號 「Nodejs技術棧」,Github 開源項目 www.nodejs.rednode

Buffer初識

在引入 TypedArray 以前,JavaScript 語言沒有用於讀取或操做二進制數據流的機制。 Buffer 類是做爲 Node.js API 的一部分引入的,用於在 TCP 流、文件系統操做、以及其餘上下文中與八位字節流進行交互。這是來自 Node.js 官網的一段描述,比較晦澀難懂,總結起來一句話 Node.js 能夠用來處理二進制流數據或者與之進行交互git

Buffer 用於讀取或操做二進制數據流,作爲 Node.js API 的一部分使用時無需 require,用於操做網絡協議、數據庫、圖片和文件 I/O 等一些須要大量二進制數據的場景。Buffer 在建立時大小已經被肯定且是沒法調整的,在內存分配這塊 Buffer 是由 C++ 層面提供而不是 V8 具體後面會講解。github

在這裏不知道你是否定爲這是很簡單的?可是上面提到的一些關鍵詞二進制流(Stream)緩衝區(Buffer),這些又都是什麼呢?下面嘗試作一些簡單的介紹。算法

什麼是二進制數據?

談到二進制咱們大腦可能會浮想到就是 010101 這種代碼命令,以下圖所示:數據庫

圖片描述

正如上圖所示,二進制數據使用 0 和 1 兩個數碼來表示的數據,爲了存儲或展現一些數據,計算機須要先將這些數據轉換爲二進制來表示。例如,我想存儲 66 這個數字,計算機會先將數字 66 轉化爲二進制 01000010 表示,印象中第一次接觸這個是在大學期間 C 語言課程中,轉換公式以下所示:api

128 64 32 16 8 4 2 1
0 1 0 0 0 0 1 0

上面用數字舉了一個示例,咱們知道數字只是數據類型之一,其它的還有字符串、圖像、文件等。例如咱們對一個英文 M 操做,在 JavaScript 裏經過 'M'.charCodeAt() 取到對應的 ASCII 碼以後(經過以上的步驟)會轉爲二進制表示。緩存

什麼是 Stream?

流,英文 Stream 是對輸入輸出設備的抽象,這裏的設備能夠是文件、網絡、內存等。安全

流是有方向性的,當程序從某個數據源讀入數據,會開啓一個輸入流,這裏的數據源能夠是文件或者網絡等,例如咱們從 a.txt 文件讀入數據。相反的當咱們的程序須要寫出數據到指定數據源(文件、網絡等)時,則開啓一個輸出流。當有一些大文件操做時,咱們就須要 Stream 像管道同樣,一點一點的將數據流出。

舉個例子

咱們如今有一大罐水須要澆一片菜地,若是咱們將水罐的水一下所有倒入菜地,首先得須要有多麼大的力氣(這裏的力氣比如計算機中的硬件性能)纔可搬得動。若是,咱們拿來了水管將水一點一點流入咱們的菜地,這個時候不要這麼大力氣就可完成。

圖片描述

經過上面的講解進一步的理解了 Stream 是什麼?那麼 Stream 和 Buffer 之間又是什麼關係呢?看如下介紹,關於 Stream 自己也有不少知識點,歡迎關注公衆號「Nodejs技術棧」,以後會單獨進行介紹。

什麼是 Buffer?

經過以上 Stream 的講解,咱們已經看到數據是從一端流向另外一端,那麼他們是如何流動的呢?

一般,數據的移動是爲了處理或者讀取它,並根據它進行決策。伴隨着時間的推移,每個過程都會有一個最小或最大數據量。若是數據到達的速度比進程消耗的速度快,那麼少數早到達的數據會處於等待區等候被處理。反之,若是數據到達的速度比進程消耗的數據慢,那麼早先到達的數據須要等待必定量的數據到達以後才能被處理。

這裏的等待區就指的緩衝區(Buffer),它是計算機中的一個小物理單位,一般位於計算機的 RAM 中。這些概念可能會很難理解,不要擔憂下面經過一個例子進一步說明。

公共汽車站乘車例子

舉一個公共汽車站乘車的例子,一般公共汽車會每隔幾十分鐘一趟,在這個時間到達以前就算乘客已經滿了,車輛也不會提早發車,早到的乘客就須要先在車站進行等待。假設到達的乘客過多,後到的一部分則須要在公共汽車站等待下一趟車駛來。

圖片描述

在上面例子中的等待區公共汽車站,對應到咱們的 Node.js 中也就是緩衝區(Buffer),另外乘客到達的速度是咱們不能控制的,咱們能控制的也只有什麼時候發車,對應到咱們的程序中就是咱們沒法控制數據流到達的時間,能夠作的是能決定什麼時候發送數據。

Buffer基本使用

瞭解了 Buffer 的一些概念以後,咱們來看下 Buffer 的一些基本使用,這裏並不會列舉全部的 API 使用,僅列舉一部分經常使用的,更詳細的可參考 Node.js 中文網

建立Buffer

在 6.0.0 以前的 Node.js 版本中, Buffer 實例是使用 Buffer 構造函數建立的,該函數根據提供的參數以不一樣方式分配返回的 Buffer new Buffer()

如今能夠經過 Buffer.from()、Buffer.alloc() 與 Buffer.allocUnsafe() 三種方式來建立

Buffer.from()

const b1 = Buffer.from('10');
const b2 = Buffer.from('10', 'utf8');
const b3 = Buffer.from([10]);
const b4 = Buffer.from(b3);

console.log(b1, b2, b3, b4); // <Buffer 31 30> <Buffer 31 30> <Buffer 0a> <Buffer 0a>
複製代碼

Buffer.alloc

返回一個已初始化的 Buffer,能夠保證新建立的 Buffer 永遠不會包含舊數據。

const bAlloc1 = Buffer.alloc(10); // 建立一個大小爲 10 個字節的緩衝區

console.log(bAlloc1); // <Buffer 00 00 00 00 00 00 00 00 00 00>
複製代碼

Buffer.allocUnsafe

建立一個大小爲 size 字節的新的未初始化的 Buffer,因爲 Buffer 是未初始化的,所以分配的內存片斷可能包含敏感的舊數據。在 Buffer 內容可讀狀況下,則可能會泄露它的舊數據,這個是不安全的,使用時要謹慎。

const bAllocUnsafe1 = Buffer.allocUnsafe(10);

console.log(bAllocUnsafe1); // <Buffer 49 ae c9 cd 49 1d 00 00 11 4f>
複製代碼

Buffer 字符編碼

經過使用字符編碼,可實現 Buffer 實例與 JavaScript 字符串之間的相互轉換,目前所支持的字符編碼以下所示:

  • 'ascii' - 僅適用於 7 位 ASCII 數據。此編碼速度很快,若是設置則會剝離高位。
  • 'utf8' - 多字節編碼的 Unicode 字符。許多網頁和其餘文檔格式都使用 UTF-8。
  • 'utf16le' - 2 或 4 個字節,小端序編碼的 Unicode 字符。支持代理對(U+10000 至 U+10FFFF)。
  • 'ucs2' - 'utf16le' 的別名。
  • 'base64' - Base64 編碼。當從字符串建立 Buffer 時,此編碼也會正確地接受 RFC 4648 第 5 節中指定的 「URL 和文件名安全字母」。
  • 'latin1' - 一種將 Buffer 編碼成單字節編碼字符串的方法(由 RFC 1345 中的 IANA 定義,第 63 頁,做爲 Latin-1 的補充塊和 C0/C1 控制碼)。
  • 'binary' - 'latin1' 的別名。
  • 'hex' - 將每一個字節編碼成兩個十六進制的字符。
const buf = Buffer.from('hello world', 'ascii');
console.log(buf.toString('hex')); // 68656c6c6f20776f726c64
複製代碼

字符串與 Buffer 類型互轉

字符串轉 Buffer

這個相信不會陌生了,經過上面講解的 Buffer.form() 實現,若是不傳遞 encoding 默認按照 UTF-8 格式轉換存儲

const buf = Buffer.from('Node.js 技術棧', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
複製代碼

Buffer 轉換爲字符串

Buffer 轉換爲字符串也很簡單,使用 toString([encoding], [start], [end]) 方法,默認編碼仍爲 UTF-8,若是不傳 start、end 可實現所有轉換,傳了 start、end 可實現部分轉換(這裏要當心了)

const buf = Buffer.from('Node.js 技術棧', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 9)); // Node.js �
複製代碼

運行查看,能夠看到以上輸出結果爲 Node.js � 出現了亂碼,爲何?

轉換過程當中爲何出現亂碼?

首先以上示例中使用的默認編碼方式 UTF-8,問題就出在這裏一箇中文在 UTF-8 下佔用 3 個字節, 這個字在 buf 中對應的字節爲 8a 80 e6 而咱們的設定的範圍爲 0~9 所以只輸出了 8a,這個時候就會形成字符被截斷出現亂碼。

下面咱們改下示例的截取範圍:

const buf = Buffer.from('Node.js 技術棧', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 11)); // Node.js 技
複製代碼

能夠看到已經正常輸出了

Buffer內存機制

Nodejs 中的 內存管理和 V8 垃圾回收機制 一節主要講解了在 Node.js 的垃圾回收中主要使用 V8 來管理,可是並無提到 Buffer 類型的數據是如何回收的,下面讓咱們來了解 Buffer 的內存回收機制。

因爲 Buffer 須要處理的是大量的二進制數據,假如用一點就向系統去申請,則會形成頻繁的向系統申請內存調用,因此 Buffer 所佔用的內存再也不由 V8 分配,而是在 Node.js 的 C++ 層面完成申請,在 JavaScript 中進行內存分配。所以,這部份內存咱們稱之爲堆外內存

注意:如下使用到的 buffer.js 源碼爲 Node.js v10.x 版本,地址:github.com/nodejs/node…

Buffer內存分配原理

Node.js 採用了 slab 機制進行預先申請、過後分配,是一種動態的管理機制。

使用 Buffer.alloc(size) 傳入一個指定的 size 就會申請一塊固定大小的內存區域,slab 具備以下三種狀態:

  • full:徹底分配狀態
  • partial:部分分配狀態
  • empty:沒有被分配狀態

8KB 限制

Node.js 以 8KB 爲界限來區分是小對象仍是大對象,在 buffer.js 中能夠看到如下代碼

Buffer.poolSize = 8 * 1024; // 102 行,Node.js 版本爲 v10.x
複製代碼

Buffer 初識 一節裏有提到過 Buffer 在建立時大小已經被肯定且是沒法調整的 到這裏應該就明白了。

Buffer 對象分配

如下代碼示例,在加載時直接調用了 createPool() 至關於直接初始化了一個 8 KB 的內存空間,這樣在第一次進行內存分配時也會變得更高效。另外在初始化的同時還初始化了一個新的變量 poolOffset = 0 這個變量會記錄已經使用了多少字節。

Buffer.poolSize = 8 * 1024;
var poolSize, poolOffset, allocPool;

... // 中間代碼省略

function createPool() {
  poolSize = Buffer.poolSize;
  allocPool = createUnsafeArrayBuffer(poolSize);
  poolOffset = 0;
}
createPool(); // 129 行
複製代碼

此時,新構造的 slab 以下所示:

圖片描述

如今讓咱們來嘗試分配一個大小爲 2048 的 Buffer 對象,代碼以下所示:

Buffer.alloc(2 * 1024)
複製代碼

如今讓咱們先看下當前的 slab 內存是怎麼樣的?以下所示:

圖片描述

那麼這個分配過程是怎樣的呢?讓咱們再看 buffer.js 另一個核心的方法 allocate(size)

// https://github.com/nodejs/node/blob/v10.x/lib/buffer.js#L318
function allocate(size) {
  if (size <= 0) {
    return new FastBuffer();
  }

  // 當分配的空間小於 Buffer.poolSize 向右移位,這裏得出來的結果爲 4KB
  if (size < (Buffer.poolSize >>> 1)) {
    if (size > (poolSize - poolOffset))
      createPool();
    var b = new FastBuffer(allocPool, poolOffset, size);
    poolOffset += size; // 已使用空間累加
    alignPool(); // 8 字節內存對齊處理
    return b;
  } else { // C++ 層面申請
    return createUnsafeBuffer(size);
  }
}
複製代碼

讀完上面的代碼,已經很清晰的能夠看到什麼時候會分配小 Buffer 對象,又什麼時候會去分配大 Buffer 對象。

Buffer 內存分配總結

這塊內容着實難理解,翻了幾本 Node.js 相關書籍,樸靈大佬的「深刻淺出 Node.js」Buffer 一節仍是講解的挺詳細的,推薦你們去閱讀下。

  1. 在初次加載時就會初始化 1 個 8KB 的內存空間,buffer.js 源碼有體現
  2. 根據申請的內存大小分爲 小 Buffer 對象大 Buffer 對象
  3. 小 Buffer 狀況,會繼續判斷這個 slab 空間是否足夠
    • 若是空間足夠就去使用剩餘空間同時更新 slab 分配狀態,偏移量會增長
    • 若是空間不足,slab 空間不足,就會去建立一個新的 slab 空間用來分配
  4. 大 Buffer 狀況,則會直接走 createUnsafeBuffer(size) 函數
  5. 不管是小 Buffer 對象仍是大 Buffer 對象,內存分配是在 C++ 層面完成,內存管理在 JavaScript 層面,最終仍是能夠被 V8 的垃圾回收標記所回收。

Buffer應用場景

如下列舉一些 Buffer 在實際業務中的應用場景,也歡迎你們在評論區補充!

I/O 操做

關於 I/O 能夠是文件或網絡 I/O,如下爲經過流的方式將 input.txt 的信息讀取出來以後寫入到 output.txt 文件,關於 Stream 與 Buffer 的關係不明白的在回頭看下 Buffer 初識 一節講解的 什麼是 Stream?什麼是 Buffer?

const fs = require('fs');

const inputStream = fs.createReadStream('input.txt'); // 建立可讀流
const outputStream = fs.createWriteStream('output.txt'); // 建立可寫流

inputStream.pipe(outputStream); // 管道讀寫
複製代碼

在 Stream 中咱們是不須要手動去建立本身的緩衝區,在 Node.js 的流中將會自動建立

zlib.js

zlib.js 爲 Node.js 的核心庫之一,其利用了緩衝區(Buffer)的功能來操做二進制數據流,提供了壓縮或解壓功能。參考源代碼 zlib.js 源碼

加解密

在一些加解密算法中會遇到使用 Buffer,例如 crypto.createCipheriv 的第二個參數 key 爲 String 或 Buffer 類型,若是是 Buffer 類型,就用到了本篇咱們講解的內容,如下作了一個簡單的加密示例,重點使用了 Buffer.alloc() 初始化一個實例(這個上面有介紹),以後使用了 fill 方法作了填充,這裏重點在看下這個方法的使用。

buf.fill(value[, offset[, end]][, encoding])

  • value: 第一個參數爲要填充的內容
  • offset: 偏移量,填充的起始位置
  • end: 結束填充 buf 的偏移量
  • encoding: 編碼集

如下爲 Cipher 的對稱加密 Demo

const crypto = require('crypto');
const [key, iv, algorithm, encoding, cipherEncoding] = [
    'a123456789', '', 'aes-128-ecb', 'utf8', 'base64'
];

const handleKey = key => {
    const bytes = Buffer.alloc(16); // 初始化一個 Buffer 實例,每一項都用 00 填充
    console.log(bytes); // <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
    bytes.fill(key, 0, 10) // 填充
    console.log(bytes); // <Buffer 61 31 32 33 34 35 36 37 38 39 00 00 00 00 00 00>

    return bytes;
}

let cipher = crypto.createCipheriv(algorithm, handleKey(key), iv);
let crypted = cipher.update('Node.js 技術棧', encoding, cipherEncoding);
    crypted += cipher.final(cipherEncoding);

console.log(crypted) // jE0ODwuKN6iaKFKqd3RF4xFZkOpasy8WfIDl8tRC5t0=
複製代碼

Buffer VS Cache

緩衝(Buffer)與緩存(Cache)的區別?

緩衝(Buffer)

緩衝(Buffer)是用於處理二進制流數據,將數據緩衝起來,它是臨時性的,對於流式數據,會採用緩衝區將數據臨時存儲起來,等緩衝到必定的大小以後在存入硬盤中。視頻播放器就是一個經典的例子,有時你會看到一個緩衝的圖標,這意味着此時這一組緩衝區並未填滿,當數據到達填滿緩衝區而且被處理以後,此時緩衝圖標消失,你能夠看到一些圖像數據。

緩存(Cache)

緩存(Cache)咱們能夠看做是一箇中間層,它能夠是永久性的將熱點數據進行緩存,使得訪問速度更快,例如咱們經過 Memory、Redis 等將數據從硬盤或其它第三方接口中請求過來進行緩存,目的就是將數據存於內存的緩存區中,這樣對同一個資源進行訪問,速度會更快,也是性能優化一個重要的點。

來自知乎的一個討論,點擊 more 查看

Buffer VS String

經過壓力測試來看看 String 和 Buffer 二者的性能如何?

const http = require('http');
let s = '';
for (let i=0; i<1024*10; i++) {
    s+='a'
}

const str = s;
const bufStr = Buffer.from(s);
const server = http.createServer((req, res) => {
    console.log(req.url);

    if (req.url === '/buffer') {
        res.end(bufStr);
    } else if (req.url === '/string') {
        res.end(str);
    }
});

server.listen(3000);
複製代碼

以上實例我放在虛擬機裏進行測試,你也能夠在本地電腦測試,使用 AB 測試工具。

測試 string

看如下幾個重要的參數指標,以後經過 buffer 傳輸進行對比

  • Complete requests: 21815
  • Requests per second: 363.58 [#/sec] (mean)
  • Transfer rate: 3662.39 [Kbytes/sec] received
$ ab -c 200 -t 60 http://192.168.6.131:3000/string
複製代碼

圖片描述

測試 buffer

能夠看到經過 buffer 傳輸總共的請求數爲 50000、QPS 達到了兩倍多的提升、每秒傳輸的字節爲 9138.82 KB,從這些數據上能夠證實提早將數據轉換爲 Buffer 的方式,可使性能獲得近一倍的提高。

  • Complete requests: 50000
  • Requests per second: 907.24 [#/sec] (mean)
  • Transfer rate: 9138.82 [Kbytes/sec] received
$ ab -c 200 -t 60 http://192.168.6.131:3000/buffer
複製代碼

圖片描述

在 HTTP 傳輸中傳輸的是二進制數據,上面例子中的 /string 接口直接返回的字符串,這時候 HTTP 在傳輸以前會先將字符串轉換爲 Buffer 類型,以二進制數據傳輸,經過流(Stream)的方式一點點返回到客戶端。可是直接返回 Buffer 類型,則少了每次的轉換操做,對於性能也是有提高的。

在一些 Web 應用中,對於靜態數據能夠預先轉爲 Buffer 進行傳輸,能夠有效減小 CPU 的重複使用(重複的字符串轉 Buffer 操做)。

Reference

歡迎你們關注「Nodejs技術棧」公衆號,掃描關注我哦!

相關文章
相關標籤/搜索