摘要:在創建時大小已經被確定且是無法調整的,在內存分配這塊是由層面提供而不是具體后面會講解。在這里不知道你是否認為這是很簡單的但是上面提到的一些關鍵詞二進制流緩沖區,這些又都是什么呢下面嘗試做一些簡單的介紹。
多數人都擁有自己不了解的能力和機會,都有可能做到未曾夢想的事情。 ——戴爾·卡耐基
從前端轉入 Node.js 的童鞋對這一部分內容會比較陌生,因為在前端中一些簡單的字符串操作已經滿足基本的業務需求,有時可能也會覺得 Buffer、Stream 這些會很神秘。回到服務端,如果你不想只做一名普通的 Node.js 開發工程師,你應該深入去學習一下 Buffer 揭開這一層神秘的面紗,同時也會讓你對 Node.js 的理解提升一個水平。
作者簡介:五月君,Nodejs Developer,熱愛技術、喜歡分享的 90 后青年,公眾號 “Nodejs技術棧”,Github 開源項目 https://www.nodejs.red
Buffer初識在引入 TypedArray 之前,JavaScript 語言沒有用于讀取或操作二進制數據流的機制。 Buffer 類是作為 Node.js API 的一部分引入的,用于在 TCP 流、文件系統操作、以及其他上下文中與八位字節流進行交互。這是來自 Node.js 官網的一段描述,比較晦澀難懂,總結起來一句話 Node.js 可以用來處理二進制流數據或者與之進行交互。
Buffer 用于讀取或操作二進制數據流,做為 Node.js API 的一部分使用時無需 require,用于操作網絡協議、數據庫、圖片和文件 I/O 等一些需要大量二進制數據的場景。Buffer 在創建時大小已經被確定且是無法調整的,在內存分配這塊 Buffer 是由 C++ 層面提供而不是 V8 具體后面會講解。
在這里不知道你是否認為這是很簡單的?但是上面提到的一些關鍵詞二進制、流(Stream)、緩沖區(Buffer),這些又都是什么呢?下面嘗試做一些簡單的介紹。
什么是二進制數據?談到二進制我們大腦可能會浮想到就是 010101 這種代碼命令,如下圖所示:
正如上圖所示,二進制數據使用 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?流,英文 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.alloc
返回一個已初始化的 Buffer,可以保證新創建的 Buffer 永遠不會包含舊數據。
const bAlloc1 = Buffer.alloc(10); // 創建一個大小為 10 個字節的緩沖區 console.log(bAlloc1); //
Buffer.allocUnsafe
創建一個大小為 size 字節的新的未初始化的 Buffer,由于 Buffer 是未初始化的,因此分配的內存片段可能包含敏感的舊數據。在 Buffer 內容可讀情況下,則可能會泄露它的舊數據,這個是不安全的,使用時要謹慎。
const bAllocUnsafe1 = Buffer.allocUnsafe(10); console.log(bAllocUnsafe1); //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); //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); //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); //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 版本,地址:https://github.com/nodejs/node/blob/v10.x/lib/buffer.js
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 個 8KB 的內存空間,buffer.js 源碼有體現
根據申請的內存大小分為 小 Buffer 對象 和 大 Buffer 對象
小 Buffer 情況,會繼續判斷這個 slab 空間是否足夠
如果空間足夠就去使用剩余空間同時更新 slab 分配狀態,偏移量會增加
如果空間不足,slab 空間不足,就會去創建一個新的 slab 空間用來分配
大 Buffer 情況,則會直接走 createUnsafeBuffer(size) 函數
不論是小 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.jszlib.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 VS Cachebytes.fill(key, 0, 10) // 填充 console.log(bytes); // 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 查看
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 操作)。
Referencehttp://nodejs.cn/api/buffer.html
深入淺出 Node.js Buffer
Do you want a better understanding of Buffer in Node.js? Check this out.
A cartoon intro to ArrayBuffers and SharedArrayBuffers
buffer.js v10.x
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106079.html
摘要:究竟是什么是一個運行時環境。對此請求的響應需要時間,但兩個用戶數據請求可以獨立并同時執行。所以這會使不太適合多線程任務。這種非阻塞消除了多線程的需要,因為服務器可以同時處理多個請求。該事件將等待毫秒,然后回調函數。系統事件來自庫的核心。 Node.js究竟是什么? Node.js是一個JavaScript運行時環境。聽起來不錯,但這是什么意思?這是如何運作的? Node運行時環境包含執...
摘要:究竟是什么是一個運行時環境。對此請求的響應需要時間,但兩個用戶數據請求可以獨立并同時執行。所以這會使不太適合多線程任務。這種非阻塞消除了多線程的需要,因為服務器可以同時處理多個請求。該事件將等待毫秒,然后回調函數。系統事件來自庫的核心。 Node.js究竟是什么? Node.js是一個JavaScript運行時環境。聽起來不錯,但這是什么意思?這是如何運作的? Node運行時環境包含執...
摘要:當某個執行完畢時,將以事件的形式通知執行操作的線程,線程執行這個事件的回調函數。為了處理異步,線程必須有事件循環,不斷的檢查有沒有未處理的事件,依次予以處理。另外,單線程帶來的好處,操作系統完全不再有線程創建銷毀的時間開銷。 前言 如果你有一定的前端基礎,比如 HTML、CSS、JavaScript、jQuery;那么,Node.js 能讓你以最低的成本快速過渡成為一個全棧工程師(我稱...
摘要:在回調隊列中,函數等待調用棧為空,因為每個語句都執行一次。最后一個運行,并且從調用棧中彈出。它將回調以先進先出順序移動到調用棧并執行。 翻譯:瘋狂的技術宅原文: https://medium.freecodecamp.o... 本文首發微信公眾號:前端先鋒歡迎關注,每天都給你推送新鮮的前端技術文章 Node.js 是一個 JavaScript 運行時環境。聽起來還不錯,不過這究竟...
閱讀 3072·2021-10-11 10:58
閱讀 1989·2021-09-24 09:47
閱讀 503·2019-08-30 14:19
閱讀 1684·2019-08-30 13:58
閱讀 1444·2019-08-29 15:26
閱讀 641·2019-08-26 13:45
閱讀 2139·2019-08-26 11:53
閱讀 1772·2019-08-26 11:30