從前端轉入 Node.js 的童鞋對這一部份內容會比較陌生,由於在前端中一些簡單的字符串操做已經知足基本的業務需求,有時可能也會以爲 Buffer、Stream 這些會很神祕。回到服務端,若是你不想只作一名普通的 Node.js 開發工程師,你應該深刻去學習一下 Buffer 揭開這一層神祕的面紗,同時也會讓你對 Node.js 的理解提高一個水平。html
緩衝(Buffer)與緩存(Cache)的區別?
在引入 TypedArray 以前,JavaScript 語言沒有用於讀取或操做二進制數據流的機制。 Buffer 類是做爲 Node.js API 的一部分引入的,用於在 TCP 流、文件系統操做、以及其餘上下文中與八位字節流進行交互。這是來自 Node.js 官網的一段描述,比較晦澀難懂,總結起來一句話 Node.js 能夠用來處理二進制流數據或者與之進行交互。前端
Buffer 用於讀取或操做二進制數據流,作爲 Node.js API 的一部分使用時無需 require,用於操做網絡協議、數據庫、圖片和文件 I/O 等一些須要大量二進制數據的場景。Buffer 在建立時大小已經被肯定且是沒法調整的,在內存分配這塊 Buffer 是由 C++ 層面提供而不是 V8 具體後面會講解。node
在這裏不知道你是否定爲這是很簡單的?可是上面提到的一些關鍵詞二進制
、流(Stream)
、緩衝區(Buffer)
,這些又都是什麼呢?下面嘗試作一些簡單的介紹。git
談到二進制咱們大腦可能會浮想到就是 010101 這種代碼命令,以下圖所示:github
正如上圖所示,二進制數據使用 0 和 1 兩個數碼來表示的數據,爲了存儲或展現一些數據,計算機須要先將這些數據轉換爲二進制來表示。例如,我想存儲 66 這個數字,計算機會先將數字 66 轉化爲二進制 01000010 表示,印象中第一次接觸這個是在大學期間 C 語言課程中,轉換公式以下所示:面試
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
上面用數字舉了一個示例,咱們知道數字只是數據類型之一,其它的還有字符串、圖像、文件等。例如咱們對一個英文 M 操做,在 JavaScript 裏經過 'M'.charCodeAt()
取到對應的 ASCII 碼以後(經過以上的步驟)會轉爲二進制表示。算法
流,英文 Stream 是對輸入輸出設備的抽象,這裏的設備能夠是文件、網絡、內存等。數據庫
流是有方向性的,當程序從某個數據源讀入數據,會開啓一個輸入流,這裏的數據源能夠是文件或者網絡等,例如咱們從 a.txt 文件讀入數據。相反的當咱們的程序須要寫出數據到指定數據源(文件、網絡等)時,則開啓一個輸出流。當有一些大文件操做時,咱們就須要 Stream 像管道同樣,一點一點的將數據流出。api
舉個例子緩存
咱們如今有一大罐水須要澆一片菜地,若是咱們將水罐的水一下所有倒入菜地,首先得須要有多麼大的力氣(這裏的力氣比如計算機中的硬件性能)纔可搬得動。若是,咱們拿來了水管將水一點一點流入咱們的菜地,這個時候不要這麼大力氣就可完成。
經過上面的講解進一步的理解了 Stream 是什麼?那麼 Stream 和 Buffer 之間又是什麼關係呢?看如下介紹,關於 Stream 自己也有不少知識點,歡迎關注公衆號「Nodejs技術棧」,以後會單獨進行介紹。
經過以上 Stream 的講解,咱們已經看到數據是從一端流向另外一端,那麼他們是如何流動的呢?
一般,數據的移動是爲了處理或者讀取它,並根據它進行決策。伴隨着時間的推移,每個過程都會有一個最小或最大數據量。若是數據到達的速度比進程消耗的速度快,那麼少數早到達的數據會處於等待區等候被處理。反之,若是數據到達的速度比進程消耗的數據慢,那麼早先到達的數據須要等待必定量的數據到達以後才能被處理。
這裏的等待區就指的緩衝區(Buffer),它是計算機中的一個小物理單位,一般位於計算機的 RAM 中。這些概念可能會很難理解,不要擔憂下面經過一個例子進一步說明。
公共汽車站乘車例子
舉一個公共汽車站乘車的例子,一般公共汽車會每隔幾十分鐘一趟,在這個時間到達以前就算乘客已經滿了,車輛也不會提早發車,早到的乘客就須要先在車站進行等待。假設到達的乘客過多,後到的一部分則須要在公共汽車站等待下一趟車駛來。
在上面例子中的等待區公共汽車站,對應到咱們的 Node.js 中也就是緩衝區(Buffer),另外乘客到達的速度是咱們不能控制的,咱們能控制的也只有什麼時候發車,對應到咱們的程序中就是咱們沒法控制數據流到達的時間,能夠作的是能決定什麼時候發送數據。
瞭解了 Buffer 的一些概念以後,咱們來看下 Buffer 的一些基本使用,這裏並不會列舉全部的 API 使用,僅列舉一部分經常使用的,更詳細的可參考 Node.js 中文網。
在 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 實例與 JavaScript 字符串之間的相互轉換,目前所支持的字符編碼以下所示:
const buf = Buffer.from('hello world', 'ascii');
console.log(buf.toString('hex')); // 68656c6c6f20776f726c64
複製代碼
字符串轉 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 技
複製代碼
能夠看到已經正常輸出了
在 Nodejs 中的 內存管理和 V8 垃圾回收機制 一節主要講解了在 Node.js 的垃圾回收中主要使用 V8 來管理,可是並無提到 Buffer 類型的數據是如何回收的,下面讓咱們來了解 Buffer 的內存回收機制。
因爲 Buffer 須要處理的是大量的二進制數據,假如用一點就向系統去申請,則會形成頻繁的向系統申請內存調用,因此 Buffer 所佔用的內存再也不由 V8 分配,而是在 Node.js 的 C++ 層面完成申請,在 JavaScript 中進行內存分配。所以,這部份內存咱們稱之爲堆外內存。
注意:如下使用到的 buffer.js 源碼爲 Node.js v10.x 版本,地址:github.com/nodejs/node…
Node.js 採用了 slab 機制進行預先申請、過後分配,是一種動態的管理機制。
使用 Buffer.alloc(size) 傳入一個指定的 size 就會申請一塊固定大小的內存區域,slab 具備以下三種狀態:
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 對象。
這塊內容着實難理解,翻了幾本 Node.js 相關書籍,樸靈大佬的「深刻淺出 Node.js」Buffer 一節仍是講解的挺詳細的,推薦你們去閱讀下。
如下列舉一些 Buffer 在實際業務中的應用場景,也歡迎你們在評論區補充!
關於 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 爲 Node.js 的核心庫之一,其利用了緩衝區(Buffer)的功能來操做二進制數據流,提供了壓縮或解壓功能。參考源代碼 zlib.js 源碼
在一些加解密算法中會遇到使用 Buffer,例如 crypto.createCipheriv 的第二個參數 key 爲 String 或 Buffer 類型,若是是 Buffer 類型,就用到了本篇咱們講解的內容,如下作了一個簡單的加密示例,重點使用了 Buffer.alloc() 初始化一個實例(這個上面有介紹),以後使用了 fill 方法作了填充,這裏重點在看下這個方法的使用。
buf.fill(value[, offset[, end]][, 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)與緩存(Cache)的區別?
緩衝(Buffer)
緩衝(Buffer)是用於處理二進制流數據,將數據緩衝起來,它是臨時性的,對於流式數據,會採用緩衝區將數據臨時存儲起來,等緩衝到必定的大小以後在存入硬盤中。視頻播放器就是一個經典的例子,有時你會看到一個緩衝的圖標,這意味着此時這一組緩衝區並未填滿,當數據到達填滿緩衝區而且被處理以後,此時緩衝圖標消失,你能夠看到一些圖像數據。
緩存(Cache)
緩存(Cache)咱們能夠看做是一箇中間層,它能夠是永久性的將熱點數據進行緩存,使得訪問速度更快,例如咱們經過 Memory、Redis 等將數據從硬盤或其它第三方接口中請求過來進行緩存,目的就是將數據存於內存的緩存區中,這樣對同一個資源進行訪問,速度會更快,也是性能優化一個重要的點。
來自知乎的一個討論,點擊 more 查看
經過壓力測試來看看 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 傳輸進行對比
$ ab -c 200 -t 60 http://192.168.6.131:3000/string
複製代碼
測試 buffer
能夠看到經過 buffer 傳輸總共的請求數爲 50000、QPS 達到了兩倍多的提升、每秒傳輸的字節爲 9138.82 KB,從這些數據上能夠證實提早將數據轉換爲 Buffer 的方式,可使性能獲得近一倍的提高。
$ ab -c 200 -t 60 http://192.168.6.131:3000/buffer
複製代碼
在 HTTP 傳輸中傳輸的是二進制數據,上面例子中的 /string 接口直接返回的字符串,這時候 HTTP 在傳輸以前會先將字符串轉換爲 Buffer 類型,以二進制數據傳輸,經過流(Stream)的方式一點點返回到客戶端。可是直接返回 Buffer 類型,則少了每次的轉換操做,對於性能也是有提高的。
在一些 Web 應用中,對於靜態數據能夠預先轉爲 Buffer 進行傳輸,能夠有效減小 CPU 的重複使用(重複的字符串轉 Buffer 操做)。
做者:五月君
連接:https://github.com/Q-Angelo/Nodejs-Roadmap
來源:Nodejs.js技術棧
複製代碼