探究不在V8堆內存中存儲的Buffer對象

前言

寫完上一篇文章想學Node.js,stream先有必要搞清楚
留下了懸念,stream對象數據流轉的具體內容是什麼?本篇文章將爲你們進行深刻講解。javascript

Buffer探究

看一段以前使用stream操做文件的例子:java

var fileName = path.resolve(__dirname, 'data.txt');
var stream=fs.createReadStream(fileName);
console.log('stream內容',stream);  
stream.on('data',function(chunk){
    console.log(chunk instanceof Buffer)
    console.log(chunk);
})

看一下打印結果,發現第一個stream是一個對象 ,截圖部份內容。node


第二個和第三個打印結果,c++


Buffer對象,相似數組,它的元素爲16進制的兩位數,即0到255的數值。能夠看出stream中流動的數據是Buffer類型,二進制數據,接下來開始咱們的Buffer探索之旅。git

什麼是二進制

二進制是計算機最底層的數據格式,字符串,數字,視頻,音頻,程序,網絡包等,在最底層都是用二進制來進行存儲。這些高級格式和二進制之間,均可以經過固定的編碼格式進行相互轉換。程序員

例如,C語言中int32類型的十進制整數(無符號),就佔用32bit即4byte,十進制的3對應的二進制就是00000000 00000000 00000000 00000011。字符串也是同理,能夠根據ASCII編碼規則或者unicode編碼規則(如utf-8)等和二進制進行相互轉換。總之,計算機底層存儲的數據都是二進制格式,各類高級類型都有對應的編碼規則和二進制進行相互轉換。github

node中爲何會出現Buffer這個模塊

在最初的javascript生態中,javascript還運行在瀏覽器端,對於處理Unicode編碼的字符串數據很容易,可是對於處理二進制以及非Unicode編碼的數據無能爲力,可是對於Server端操做TCP/HTTP以及文件I/O的處理是必須的。我想就是所以在Node.js裏面提供了Buffer類處理二進制的數據,能夠處理各類類型的數據。數據庫

Buffer模塊的一個說明。segmentfault

在Node.js裏面一些重要模塊net、http、fs中的數據傳輸以及處理都有Buffer的身影,由於一些基礎的核心模塊都要依賴Buffer,因此在node啓動的時候,就已經加載了Buffer,咱們能夠在全局下面直接使用Buffer,無需經過require()。且 Buffer 的大小在建立時肯定,沒法調整。

Buffer建立

NodeJS v6.0.0版本以前,Buffer實例是經過 Buffer 構造函數建立的,即便用 new 關鍵字建立,它根據提供的參數返回不一樣的 Buffer,但在以後的版本中這種聲明方式就被廢棄了,替代 new 的建立方式主要有如下幾種。後端

1. Buffer.alloc 和 Buffer.allocUnsafe(建立固定大小的buffer)

Buffer.allocBuffer.allocUnsafe 建立 Buffer 的傳參方式相同,參數爲建立 Buffer 的長度,數值類型。

// Buffer.alloc 和 Buffer.allocUnsafe 建立 Buffer
// Buffer.alloc 建立 Buffer,建立一個大小爲6字節的空buffer,通過了初始化
let buf1 = Buffer.alloc(6);

// Buffer.allocUnsafe 建立 Buffer,建立一個大小爲6字節的buffer,未通過初始化
let buf2 = Buffer.allocUnsafe(6);

console.log(buf1); // <Buffer 00 00 00 00 00 00>
console.log(buf2); // <Buffer 00 e7 8f a0 00 00>

經過代碼能夠看出,用 Buffer.allocBuffer.allocUnsafe 建立 Buffer 是有區別的,Buffer.alloc 建立的 Buffer 是被初始化過的,即 Buffer 的每一項都用 00 填充,而 Buffer.allocUnsafe 建立的 Buffer 並無通過初始化,在內存中只要有閒置的 Buffer 就直接 「抓過來」 使用。

Buffer.allocUnsafe 建立 Buffer 使得內存的分配很是快,但已分配的內存段可能包含潛在的敏感數據,有明顯性能優點的同時又是不安全的,因此使用需格外 「當心」。

二、Buffer.from(根據內容直接建立Buffer)

Buffer.from(str, )
支持三種傳參方式:
  • 第一個參數爲字符串,第二個參數爲字符編碼,如 ASCIIUTF-8Base64 等等。
  • 傳入一個數組,數組的每一項會以十六進制存儲爲 Buffer 的每一項。
  • 傳入一個 Buffer,會將 Buffer 的每一項做爲新返回 Buffer 的每一項。

說明:Buffer目前支持的編碼格式

  • ascii - 僅支持7位ASCII數據。
  • utf8 - 多字節編碼的Unicode字符
  • utf16le - 2或4個字節,小端編碼的Unicode字符
  • base64 - Base64字符串編碼
  • binary - 二進制編碼。
  • hex - 將每一個字節編碼爲兩個十六進制字符。
傳入字符串和字符編碼:
// 傳入字符串和字符編碼
let buf = Buffer.from("hello", "utf8");

console.log(buf); // <Buffer 68 65 6c 6c 6f>
傳入數組:
// 數組成員爲十進制數
let buf = Buffer.from([1, 2, 3]);

console.log(buf); // <Buffer 01 02 03>
// 數組成員爲十六進制數
let buf = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]);

console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString("utf8")); // 你好

NodeJS 中不支持 GB2312 編碼,默認支持 UTF-8,在 GB2312 中,一個漢字佔兩個字節,而在 UTF-8 中,一個漢字佔三個字節,因此上面 「你好」 的 Buffer 爲 6 個十六進制數組成。

// 數組成員爲字符串類型的數字
let buf = Buffer.from(["1", "2", "3"]);
console.log(buf); // <Buffer 01 02 03>

傳入的數組成員能夠是任何進制的數值,當成員爲字符串的時候,若是值是數字會被自動識別成數值類型,若是值不是數字或成員爲是其餘非數值類型的數據,該成員會被初始化爲 00。

建立的 Buffer 能夠經過 toString 方法直接指定編碼進行轉換,默認編碼爲 UTF-8

傳入 Buffer:
// 傳入一個 Buffer
let buf1 = Buffer.from("hello", "utf8");

let buf2 = Buffer.from(buf1);

console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // false
console.log(buf1[0] === buf2[0]); // true
buf1[1]=12;
console.log(buf1); // <Buffer 68 0c 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>

當傳入的參數爲一個 Buffer 的時候,會建立一個新的 Buffer 並複製上面的每個成員。

Buffer 爲引用類型,一個 Buffer 複製了另外一個 Buffer 的成員,當其中一個 Buffer 複製的成員有更改,另外一個 Buffer 對應的成員不會跟着改變,說明傳入buffer建立新的Buffer的時候是一個深拷貝的過程。

Buffer的內存分配機制

buffer對應於 V8 堆內存以外的一塊原始內存

Buffer是一個典型的javascriptC++結合的模塊,與性能有關的用C++來實現,javascript 負責銜接和提供接口。Buffer所佔的內存不是V8堆內存,是獨立於V8堆內存以外的內存,經過C++層面實現內存申請(能夠說真正的內存是C++層面提供的)、javascript 分配內存(能夠說JavaScript層面只是使用它)。Buffer在分配內存最終是使用ArrayBuffer對象做爲載體。簡單點而言, 就是Buffer模塊使用v8::ArrayBuffer分配一片內存,經過TypedArray中的v8::Uint8Array來去寫數據。

內存分配的8K機制

  • 分配小內存

說道Buffer的內存分配就不得不說Buffer8KB的問題,對應buffer.js源碼裏面的處理以下:

Buffer.poolSize = 8 * 1024;

function allocate(size)
{
    if(size <= 0 )
        return new FastBuffer();
    if(size < Buffer.poolSize >>> 1 )
        if(size > poolSize - poolOffset)
            createPool();
        var b = allocPool.slice(poolOffset,poolOffset + size);
        poolOffset += size;
        alignPool();
        return b
    } else {
        return createUnsafeBuffer(size);
    }
}

源碼直接看來就是以8KB做爲界限,若是寫入的數據大於8KB一半的話直接則直接去分配內存,若是小於4KB的話則從當前分配池裏面判斷是否夠空間放下當前存儲的數據,若是不夠則從新去申請8KB的內存空間,把數據存儲到新申請的空間裏面,若是足夠寫入則直接寫入數據到內存空間裏面,下圖爲其內存分配策略。

Buffer內存分配策略圖
看內存分配策略圖,若是當前存儲了2KB的數據,後面要存儲5KB大小數據的時候分配池判斷所需內存空間大於4KB,則會去從新申請內存空間來存儲5KB數據而且分配池的當前偏移指針也是指向新申請的內存空間,這時候就以前剩餘的6KB(8KB-2KB)內存空間就會被擱置。至於爲何會用8KB做爲存儲單元分配,爲何大於8KB按照大內存分配策略,在下面Buffer內存分配機制優勢有說明。

  • 分配大內存

仍是看上面那張內存分配圖,若是須要超過8KBBuffer對象,將會直接分配一個SlowBuffer對象做爲基礎單元,這個基礎單元將會被這個大Buffer對象獨佔。

// Big buffer,just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

這裏的SlowBUffer類實在C++中定義的,雖然引用buffer模塊能夠訪問到它,可是不推薦直接操做它,而是用Buffer替代。這裏內部parent屬性指向的SlowBuffer對象來自Node自身C++中的定義,是C++層面的Buffer對象,所用內存不在V8的堆中

  • 內存分配的限制

此外,Buffer單次的內存分配也有限制,而這個限制根據不一樣操做系統而不一樣,而這個限制能夠看到node_buffer.h裏面

static const unsigned int kMaxLength =
    sizeof(int32_t) == sizeof(intptr_t) ? 0x3fffffff : 0x7fffffff;

對於32位的操做系統單次可最大分配的內存爲1G,對於64位或者更高的爲2G。

buffer內存分配機制優勢

Buffer真正的內存實在NodeC++層面提供的,JavaScript層面只是使用它。當進行小而頻繁的Buffer操做時,採用的是8KB爲一個單元的機制進行預先申請和過後分配,使得Javascript到操做系統之間沒必要有過多的內存申請方面的系統調用。對於大塊的Buffer而言(大於8KB),則直接使用C++層面提供的內存,則無需細膩的分配操做。

Buffer與stream

stream的流動爲何要使用二進制Buffer

根據最初代碼的打印結果,stream中流動的數據就是Buffer類型,也就是二進制

緣由一:

node官方使用二進制做爲數據流動確定是考慮過不少,好比在上一篇 想學Node.js,stream先有必要搞清楚文章已經說過,stream主要的設計目的——是爲了優化IO操做文件IO網絡IO),對應後端不管是文件IO仍是網絡IO,其中包含的數據格式都是未知的,有多是字符串,音頻,視頻,網絡包等等,即便就是字符串,它的編碼格式也是未知的,可能ASC編碼,也可能utf-8編碼,對於這些未知的狀況,還不如直接使用最通用的格式二進制.

緣由二:

Buffer對於http請求也會帶來性能提高。

舉一個例子:

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const fileName = path.resolve(__dirname, 'buffer-test.txt');
    fs.readFile(fileName, function (err, data) {
        res.end(data)   // 測試1 :直接返回二進制數據
        // res.end(data.toString())  // 測試2 :返回字符串數據
    });
});
server.listen(8000);

將代碼中的buffer-test文件大小增長到50KB左右,而後使用ab工具測試一下性能,你會發現不管是從吞吐量(Requests per second)仍是鏈接時間上,返回二進制格式比返回字符串格式效率提升不少。爲什麼字符串格式效率低?—— 由於網絡請求的數據原本就是二進制格式傳輸,雖然代碼中寫的是 response 返回字符串,最終還得再轉換爲二進制進行傳輸,多了一步操做,效率固然低了。

Buffer在stream數據流轉充當的角色

咱們能夠把整個流(stream)Buffer的配合過程看做公交站。在一些公交站,公交車在沒有裝滿乘客前是不會發車的,或者在特定的時刻纔會發車。固然,乘客也可能在不一樣的時間,人流量大小也會有所不一樣,有人多的時候,有人少的時候,乘客公交車站都沒法控制人流量。

不論什麼時候,早到的乘客都必須等待,直到公交車接到指令能夠發車。當乘客到站,發現公交車已經裝滿,或者已經開走,他就必須等待下一班車次。

總之,這裏總會有一個等待的地方,這個等待的區域就是Node.js中的BufferNode.js不能控制數據何時傳輸過來,傳輸速度,就好像公交車站沒法控制人流量同樣。他只能決定何時發送數據(公交車發車)。若是時間還不到,那麼Node.js就會把數據放入Buffer等待區域中,一個在RAM中的地址,直到把他們發送出去進行處理。

注意點:

Buffer雖好也不要瞎用,BufferString二者均可以存儲字符串類型的數據,可是,StringBuffer不一樣,在內存分配上面,String直接使用v8堆存儲,不用通過c++堆外分配內存,而且Google也對String進行優化,在實際的拼接測速對比中,StringBuffer快。可是Buffer的出現是爲了處理二進制以及其餘非Unicode編碼的數據,因此在處理非utf8數據的時候須要使用到Buffer來處理。

今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。

Node系列原創文章:

深刻理解Node.js 中的進程與線程

想學Node.js,stream先有必要搞清楚

require時,exports和module.exports的區別你真的懂嗎

源碼解讀一文完全搞懂Events模塊

Node.js 高級進階之 fs 文件模塊學習

關注我

以爲不錯點個Star,歡迎 加羣 互相學習。

做者簡介:koala,專一完整的 Node.js 技術棧分享,從 JavaScript 到 Node.js,再到後端數據庫,祝您成爲優秀的高級 Node.js 工程師。【程序員成長指北】做者,Github 博客開源項目 https://github.com/koala-coding/goodBlog
相關文章
相關標籤/搜索