多數人都擁有本身不瞭解的能力和機會,都有可能作到不曾夢想的事情。 ——戴爾·卡耐基html
從前端轉入 Node.js 的童鞋對這一部份內容會比較陌生,由於在前端中一些簡單的字符串操做已經知足基本的業務需求,有時可能也會以爲 Buffer、Stream 這些會很神祕。回到服務端,若是你不想只作一名普通的 Node.js 開發工程師,你應該深刻去學習一下 Buffer 揭開這一層神祕的面紗,同時也會讓你對 Node.js 的理解提高一個水平。前端
做者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 後青年,公衆號 「Nodejs技術棧」,Github 開源項目 www.nodejs.rednode
在引入 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 是對輸入輸出設備的抽象,這裏的設備能夠是文件、網絡、內存等。安全
流是有方向性的,當程序從某個數據源讀入數據,會開啓一個輸入流,這裏的數據源能夠是文件或者網絡等,例如咱們從 a.txt 文件讀入數據。相反的當咱們的程序須要寫出數據到指定數據源(文件、網絡等)時,則開啓一個輸出流。當有一些大文件操做時,咱們就須要 Stream 像管道同樣,一點一點的將數據流出。
舉個例子
咱們如今有一大罐水須要澆一片菜地,若是咱們將水罐的水一下所有倒入菜地,首先得須要有多麼大的力氣(這裏的力氣比如計算機中的硬件性能)纔可搬得動。若是,咱們拿來了水管將水一點一點流入咱們的菜地,這個時候不要這麼大力氣就可完成。
經過上面的講解進一步的理解了 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 操做)。
歡迎你們關注「Nodejs技術棧」公衆號,掃描關注我哦!