【深刻淺出Node.js系列六】Buffer那些事兒

#0 系列目錄#html

Javascript是爲瀏覽器而設計的,能很好的處理unicode編碼的字符串,但對於二進制或非unicode編碼的數據就顯得無能爲力。Node.js繼承Javascript的語言特性,同時又擴展了Javascript語言,爲二進制的數據處理提供了Buffer類,讓Node.js能夠像其餘程序語言同樣,能處理各類類型的數據了。node

#1 Buffer介紹# 在Node.js中,Buffer類是隨Node內核一塊兒發佈的核心庫。Buffer庫爲Node.js帶來了一種存儲原始數據的方法,可讓Nodejs處理二進制數據,每當須要在Nodejs中處理I/O操做中移動的數據時,就有可能使用Buffer庫。原始數據存儲在 Buffer 類的實例中。一個 Buffer 相似於一個整數數組,但它對應於 V8 堆內存以外的一塊原始內存。git

Buffer 和 Javascript 字符串對象之間的轉換須要顯式地調用編碼方法來完成。如下是幾種不一樣的字符串編碼:github

‘ascii’ – 僅用於 7 位 ASCII 字符。這種編碼方法很是快,而且會丟棄高位數據。npm

‘utf8’ – 多字節編碼的 Unicode 字符。許多網頁和其餘文件格式使用 UTF-8。api

‘ucs2’ – 兩個字節,以小尾字節序(little-endian)編碼的 Unicode 字符。它只能對 BMP(基本多文種平面,U+0000 – U+FFFF) 範圍內的字符編碼。數組

‘base64’ – Base64 字符串編碼。瀏覽器

‘binary’ – 一種將原始二進制數據轉換成字符串的編碼方式,僅使用每一個字符的前 8 位。這種編碼方法已通過時,應當儘量地使用 Buffer 對象。Node 的後續版本將會刪除這種編碼。緩存

Buffer官方文檔:http://nodejs.org/api/buffer.html網絡

#2 你該當心Buffer啦# 像許多計算機的技術同樣,都是從國外傳播過來的。那些以英文做爲母語的傳 道者們應該沒有考慮過英文之外的使用者,因此你有可能看到以下這樣一段代碼在向你描述如何在data事件中鏈接字符串。

var fs = require('fs');
var rs = fs.createReadStream('testdata.md'); 
var data = '';
rs.on("data", function (trunk) {
    data += trunk; 
});
rs.on("end", function () { 
    console.log(data);
});

若是這個文件讀取流讀取的是一個純英文的文件,這段代碼是可以正常輸出的。可是若是咱們再改變一下條件,將每次讀取的buffer大小變成一個奇數,以模擬一個字符被分配在兩個trunk中的場景

var rs = fs.createReadStream('testdata.md', {bufferSize: 11});

咱們將會獲得如下這樣的亂碼輸出:

事件循���和請求���象構成了Node.js���異步I/O模 型的���個基本���素,這也是典���的消費���生產者場景。

形成這個問題的根源在於data += trunk語句裏隱藏的錯誤,在默認的狀況下,trunk是一個Buffer對象。這句話的實質是隱藏了toString的變換的:

data = data.toString() + trunk.toString();

因爲漢字不是用一個字節來存儲的,致使有被截破的漢字的存在,因而出現亂碼。解決這個問題有一個簡單的方案,是設置編碼集:

var rs = fs.createReadStream('testdata.md', {encoding: 'utf-8', bufferSize: 11});

這將獲得一個正常的字符串響應:

事件循環和請求對象構成了Node.js的異步I/O模型的兩個基本元素 ,這也是典型的消費者生產者場景。

遺憾的是目前Node.js僅支持hex、utf八、ascii、binary、base6四、ucs2幾種編碼的轉換。對於那些由於歷史遺留問題依舊還生存着的GBK,GB2312等編碼, 該方法是無能爲力的

#3 有趣的string_decoder# 在這個例子中,若是仔細觀察,會發現一件有趣的事情發生在設置編碼集以後。咱們提到data += trunk等價於data = data.toString() + trunk.toString()。經過如下的代碼能夠測試到一個漢字佔用三個字節,而咱們按11個字節來截取trunk的話,依舊會存在一個漢字被分割在兩個trunk中的情景。

console.log("事件循環和請求對象".length); 
console.log(new Buffer("事件循環和請求對象").length);

按照猜測的toString()方式,應該返回的是事件循xxx和請求xxx象纔對,其 中「環」字應該變成亂碼纔對,可是在設置了encoding(默認的utf8)以後,結果卻正常顯示了,這個結果十分有趣。

輸入圖片說明

在好奇心的驅使下能夠探查到data事件調用了string_decoder來進行編碼補足的行爲。經過string_decoder對象輸出第一個截取Buffer(事件循xx)時,只返回事件循這個字符串,保留xx。第二次經過string_decoder對象輸出時檢測到上次保留的xx,將上次剩餘內容和本次的Buffer進行從新拼接輸出。因而達到正常輸出的目的。

string_decoder,目前在文件流讀取和網絡流讀取中都有應用到,必定程度上避免了粗魯拼接trunk致使的亂碼錯誤。可是,遺憾在於string_decoder目前只支持utf8編碼。它的思路其實還能夠擴展到其餘編碼上,只是最終是否會支持目前尚不可得知。

#4 鏈接Buffer對象的正確方法# 那麼萬能的適應各類編碼並且正確的拼接Buffer對象的方法是什麼呢?咱們從 Node.js在github上的源碼中找出這樣一段正確讀取文件,並鏈接buffer對象的方法:

var buffers = [];
var nread = 0;
readStream.on('data', function (chunk) {
    buffers.push(chunk);
    nread += chunk.length;
});
readStream.on('end', function () { 
    var buffer = null; 
    switch(buffers.length) {
        case 0: 
            buffer = new Buffer(0); 
            break;
        case 1: 
            buffer = buffers[0]; 
            break;
        default:
            buffer = new Buffer(nread);
            for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {
                var chunk = buffers[i];
                chunk.copy(buffer, pos); 
                pos += chunk.length;
            } 
            break;
    }
});

在end事件中經過細膩的鏈接方式,最後拿到理想的Buffer對象。這時候不管是在支持的編碼之間轉換,仍是在不支持的編碼之間轉換(利用iconv模塊轉換),都不會致使亂碼。

#5 簡化鏈接Buffer對象的過程# 上述一大段代碼僅只完成了一件事情,就是鏈接多個Buffer對象,而這種場景需求將會在多個地方發生,因此,採用一種更優雅的方式來完成該過程是必要的。筆者基於以上的代碼封裝出一個bufferhelper模塊,用於更簡潔地處理Buffer對象。能夠經過NPM進行安裝:

npm install bufferhelper

下面的例子演示瞭如何調用這個模塊。與傳統data += trunk之間只是bufferHelper.concat(chunk)的差異,既避免了錯誤的出現,又使得代碼能夠獲得簡化而有效地編寫。

var http = require('http');
var BufferHelper = require('bufferhelper'); 
http.createServer(function (request, response) {
    var bufferHelper = new BufferHelper(); 
    request.on("data", function (chunk) { 
        bufferHelper.concat(chunk);
    });
    request.on('end', function () {
        var html = bufferHelper.toBuffer().toString() ;
        response.writeHead(200); 
        response.end(html);
    }); 
}).listen(8001);

因此關於Buffer對象的操做的最佳實踐是:

保持編碼不變,以利於後續編碼轉換

使用封裝方法達到簡潔代碼的目的

#6 Buffer的基本使用# Buffer的基本使用,主要就是API所提供的操做,主要包括3個部分建立Buffer類、讀Buffer、寫Buffer

##6.1 建立Buffer類## 要建立一個Buffer的實例,咱們要經過new Buffer來建立。新建文件buffer_new.js。

~ vi buffer_new.js

// 長度爲0的Buffer實例
var a = new Buffer(0);
console.log(a);
> <Buffer >

// 長度爲0的Buffer實例相同,a1,a2是一個實例
var a2 = new Buffer(0);
console.log(a2);
> <Buffer >

// 長度爲10的Buffer實例
var a10 = new Buffer(10);
console.log(a10);
> <Buffer 22 37 02 00 00 00 00 04 00 00>

// 數組
var b = new Buffer(['a','b',12])
console.log(b);
> <Buffer 00 00 0c>

// 字符編碼
var b2 = new Buffer('你好','utf-8');
console.log(b2);
> <Buffer e4 bd a0 e5 a5 bd>

Buffer類有5個類方法,用於Buffer類的輔助操做。

  1. 編碼檢查,上文中提到Buffer和Javascript字符串轉換時,須要顯式的設置編碼,那麼這幾種編碼類型是Buffer所支持的。像中文處理只能使用utf-8編碼,對於幾年前經常使用的gbk,gb2312等編碼是沒法解析的。
// 支持的編碼
console.log(Buffer.isEncoding('utf-8'))
console.log(Buffer.isEncoding('binary'))
console.log(Buffer.isEncoding('ascii'))
console.log(Buffer.isEncoding('ucs2'))
console.log(Buffer.isEncoding('base64'))
console.log(Buffer.isEncoding('hex'))  # 16制進
> true

//不支持的編碼
console.log(Buffer.isEncoding('gbk'))
console.log(Buffer.isEncoding('gb2312'))
> false
  1. Buffer檢查,不少時候咱們須要判斷數據的類型,對應後續的操做。
// 是Buffer類
console.log(Buffer.isBuffer(new Buffer('a')))
> true

// 不是Buffer
console.log(Buffer.isBuffer('adfd'))
console.log(Buffer.isBuffer('\u00bd\u00bd'))
> false
  1. 字符串的字節長度,因爲字符串編碼不一樣,因此字符串長度和字節長度有時是不同的。好比,1箇中文字符是3個字節,經過utf-8編碼輸出就是4箇中文字符,佔12個字節。
var str2 = '粉絲日誌';
console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'utf8') + " bytes");
> 粉絲日誌: 4 characters, 12 bytes
console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'ascii') + " bytes");
> 粉絲日誌: 4 characters, 4 bytes
  1. Buffer的鏈接,用於鏈接Buffer的數組。咱們能夠手動分配Buffer對象合併後的Buffer空間大小,若是Buffer空間不夠了,則數據會被截斷。
var b1 = new Buffer("abcd");
var b2 = new Buffer("1234");
var b3 = Buffer.concat([b1,b2],8);
console.log(b3.toString());
> abcd1234

var b4 = Buffer.concat([b1,b2],32);
console.log(b4.toString());
console.log(b4.toString('hex'));//16進制輸出
> abcd1234   亂碼....
> 616263643132333404000000000000000000000000000000082a330200000000

var b5 = Buffer.concat([b1,b2],4);
console.log(b5.toString());
> abcd
  1. Buffer的比較,用於Buffer的內容排序,按字符串的順序。
var a1 = new Buffer('10');
var a2 = new Buffer('50');
var a3 = new Buffer('123');

// a1小於a2
console.log(Buffer.compare(a1,a2));
> -1

// a2小於a3
console.log(Buffer.compare(a2,a3));
> 1

// a1,a2,a3排序輸出
console.log([a1,a2,a3].sort(Buffer.compare));
> [ <Buffer 31 30>, <Buffer 31 32 33>, <Buffer 35 30> ]

// a1,a2,a3排序輸出,以utf-8的編碼輸出
console.log([a1,a2,a3].sort(Buffer.compare).toString());
> 10,123,50

##6.2 寫入Buffer## 把數據寫入到Buffer的操做,新建文件buffer_write.js。

~ vi buffer_write.js

//////////////////////////////
// Buffer寫入
//////////////////////////////

// 建立空間大小爲64字節的Buffer
var buf = new Buffer(64);

// 從開始寫入Buffer,偏移0
var len1 = buf.write('從開始寫入');

// 打印數據的長度,打印Buffer的0到len1位置的數據
console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));

// 從新寫入Buffer,偏移0,將覆蓋以前的Buffer內存
len1 = buf.write('從新寫入');
console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));

// 繼續寫入Buffer,偏移len1,寫入unicode的字符串
var len2 = buf.write('\u00bd + \u00bc = \u00be',len1);
console.log(len2 + " bytes: " + buf.toString('utf8', 0, len1+len2));

// 繼續寫入Buffer,偏移30
var len3 = buf.write('從第30位寫入', 30);
console.log(len3 + " bytes: " + buf.toString('utf8', 0, 30+len3));

// Buffer總長度和數據
console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));

// 繼續寫入Buffer,偏移30+len3
var len4 = buf.write('寫入的數據長度超過Buffer的總長度!',30+len3);

// 超過Buffer空間的數據,沒有被寫入到Buffer中
console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));

輸入圖片說明

Node.js的節點的緩衝區,根據讀寫整數的範圍,提供了不一樣寬度的支持,使從1到8個字節(8位、16位、32位)的整數、浮點數(float)、雙精度浮點數(double)能夠被訪問,分別對應不一樣的writeXXX()函數,使用方法與buf.write()相似。

buf.write(string[, offset][, length][, encoding])
buf.writeUIntLE(value, offset, byteLength[, noAssert])
buf.writeUIntBE(value, offset, byteLength[, noAssert])
buf.writeIntLE(value, offset, byteLength[, noAssert])
buf.writeIntBE(value, offset, byteLength[, noAssert])
buf.writeUInt8(value, offset[, noAssert])
buf.writeUInt16LE(value, offset[, noAssert])
buf.writeUInt16BE(value, offset[, noAssert])
buf.writeUInt32LE(value, offset[, noAssert])
buf.writeUInt32BE(value, offset[, noAssert])
buf.writeInt8(value, offset[, noAssert])
buf.writeInt16LE(value, offset[, noAssert])
buf.writeInt16BE(value, offset[, noAssert])
buf.writeInt32LE(value, offset[, noAssert])
buf.writeInt32BE(value, offset[, noAssert])
buf.writeFloatLE(value, offset[, noAssert])
buf.writeFloatBE(value, offset[, noAssert])
buf.writeDoubleLE(value, offset[, noAssert])
buf.writeDoubleBE(value, offset[, noAssert])

另外,關於Buffer寫入操做,還有一些Buffer類的原型函數能夠操做。Buffer複製函數 buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd])

// 新建兩個Buffer實例
var buf1 = new Buffer(26);
var buf2 = new Buffer(26);

// 分別向2個實例中寫入數據
for (var i = 0 ; i < 26 ; i++) {
    buf1[i] = i + 97; // 97是ASCII的a
    buf2[i] = 50; // 50是ASCII的2
}

// 把buf1的內存複製給buf2
buf1.copy(buf2, 5, 0, 10); // 從buf2的第5個字節位置開始插入,複製buf1的從0-10字節的數據到buf2中
console.log(buf2.toString('ascii', 0, 25)); // 輸入buf2的0-25字節
> 22222abcdefghij2222222222

Buffer填充函數 buf.fill(value[, offset][, end])。

// 新建Buffer實例,長度20節節
var buf = new Buffer(20);

// 向Buffer中填充數據
buf.fill("h");
console.log(buf)
> <Buffer 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68>
console.log("buf:"+buf.toString())
> buf:hhhhhhhhhhhhhhhhhhhh
// 清空Buffer中的數據
buf.fill();
console.log("buf:"+buf.toString())
> buf:

Buffer裁剪,buf.slice([start][, end])。返回一個新的緩衝區,它和舊緩衝區指向同一塊內存,可是從索引 start 到 end 的位置剪裁

var buf1 = new Buffer(26);
for (var i = 0 ; i < 26 ; i++) {
    buf1[i] = i + 97;
}

// 從剪切buf1中的0-3的位置的字節,新生成的buf2是buf1的一個切片。
var buf2 = buf1.slice(0, 3);
console.log(buf2.toString('ascii', 0, buf2.length));
> abc

// 當修改buf1時,buf2同時也會發生改變
buf1[0] = 33;
console.log(buf2.toString('ascii', 0, buf2.length));
> !bc

##6.3 讀取Buffer## 咱們把數據寫入Buffer後,咱們還須要把數據從Buffer中讀出來,新建文件buffer_read.js。咱們能夠經過readXXX()函數得到對應該寫入時編碼的索引值,再轉換原始值取出,有這種方法操做中文字符就會變得麻煩,最經常使用的讀取Buffer的方法,其實就是toString()

~ vi buffer_read.js

//////////////////////////////
// Buffer 讀取
//////////////////////////////

var buf = new Buffer(10);
for (var i = 0 ; i < 10 ; i++) {
    buf[i] = i + 97;
}
console.log(buf.length + " bytes: " + buf.toString('utf-8'));
> 10 bytes: abcdefghij

// 讀取數據
for (ii = 0; ii < buf.length; ii++) {
    var ch = buf.readUInt8(ii); // 得到ASCII索引
    console.log(ch + ":"+ String.fromCharCode(ch));
}
> 97:a
98:b
99:c
100:d
101:e
102:f
103:g
104:h
105:i
106:j

寫入中文數據,以readXXX進行讀取,會3個字節來表示一箇中文字。

var buf = new Buffer(10);
buf.write('abcd')
buf.write('數據',4)
for (var i = 0; i < buf.length; i++) {
    console.log(buf.readUInt8(i));
}

>97
98
99
100
230  // 230,149,176 表明「數」
149
176
230  // 230,141,174 表明「據」
141
174

若是想輸出正確的中文,那麼咱們能夠用toString(‘utf-8’)的函數來操做。

console.log("buffer :"+buf); // 默認調用了toString()的函數
> buffer :abcd數據
console.log("utf-8  :"+buf.toString('utf-8'));
> utf-8  :abcd數據
console.log("ascii  :"+buf.toString('ascii'));//有亂碼,中文不能被正確解析
> ascii  :abcdf 0f
.
console.log("hex    :"+buf.toString('hex')); //16進制
> hex    :61626364e695b0e68dae

對於Buffer的輸出,咱們用的最多的操做就是toString(),按照存入的編碼進行讀取。除了toString()函數,還能夠用toJSON()直接Buffer解析成JSON對象

var buf = new Buffer('test');
console.log(buf.toJSON());
> { type: 'Buffer', data: [ 116, 101, 115, 116 ] }

#7 Buffer的性能測試# ##7.1 8K的建立測試## 每次咱們建立一個新的Buffer實例時,都會檢查當前Buffer的內存池是否已經滿,當前內存池對於新建的Buffer實例是共享的,內存池的大小爲8K

若是新建立的Buffer實例大於8K時,就把Buffer交給SlowBuffer實例存儲若是新建立的Buffer實例小於8K,同時小於當前內存池的剩餘空間,那麼這個Buffer存入當前的內存池若是Buffer實例不大於0,則統一返回默認的zerobuffer實例

下面咱們建立2個Buffer實例,第一個是以4k爲空間,第二個以4.001k爲空間,循環建立10萬次。

var num = 100*1000;
console.time("test1");
for(var i=0;i<num;i++){
    new Buffer(1024*4);
}
console.timeEnd("test1");
> test1: 132ms

console.time("test2");
for(var j=0;j<num;j++){
    new Buffer(1024*4+1);
}
console.timeEnd("test2");
> test2: 163ms

第二個以4.001k爲空間的耗時多23%,這就意味着第二個,每二次循環就要從新申請一次內存池的空間。這是須要咱們很是注意的。

##7.2 多Buffer仍是單一Buffer## 當咱們須要對數據進行緩存時,建立多個小的Buffer實例好,仍是建立一個大的Buffer實例好?好比咱們要建立1萬個長度在1-2048之間不等的字符串。

var max = 2048;     //最大長度
var time = 10*1000; //循環1萬次

// 根據長度建立字符串
function getString(size){
    var ret = ""
    for(var i=0;i<size;i++) ret += "a";
    return ret;
}

// 生成字符串數組,1萬條記錄
var arr1=[];
for(var i=0;i<time;i++){
    var size = Math.ceil(Math.random()*max)
    arr1.push(getString(size));
}
//console.log(arr1);

// 建立1萬個小Buffer實例
console.time('test3');
var arr_3 = [];
for(var i=0;i<time;i++){
    arr_3.push(new Buffer(arr1[i]));
}
console.timeEnd('test3');
> test3: 217ms

// 建立一個大實例,和一個offset數組用於讀取數據。
console.time('test4');
var buf = new Buffer(time*max);
var offset=0;
var arr_4=[];
for(var i=0;i<time;i++){
    arr_4[i]=offset;
    buf.write(arr1[i],offset,arr1[i].length);
    offset=offset+arr1[i].length;
}
console.timeEnd('test4');
> test4: 12ms

讀取索引爲2的數據:

console.log("src:[2]="+arr1[2]);
console.log("test3:[2]="+arr_3[2].toString());
console.log("test4:[2]="+buf.toString('utf-8',arr_4[2],arr_4[3]));

運行結果如圖所示:

輸入圖片說明

對於這類的需求來講,提早生成一個大的Buffer實例進行存儲,要比每次生成小的Buffer實例高效的多,能提高一個數量級的計算效率。因此,理解並用好Buffer是很是重要的!!

##7.3 string VS Buffer## 有了Buffer咱們是否需求把全部String的鏈接,都換成Buffer的鏈接?那麼咱們就須要測試一下,String和Buffer作字符串鏈接時,哪一個更快一點?

下面咱們進行字符串鏈接,循環30萬次:

//測試三,Buffer VS string
var time = 300*1000;
var txt = "aaa"

var str = "";
console.time('test5')
for(var i=0;i<time;i++){
    str += txt;
}
console.timeEnd('test5')
> test5: 24ms

console.time('test6')
var buf = new Buffer(time * txt.length)
var offset = 0;
for(var i=0;i<time;i++){
    var end = offset + txt.length;
    buf.write(txt,offset,end);
    offset=end;
}
console.timeEnd('test6')
> test6: 85ms

從測試結果,咱們能夠明顯的看到,String對字符串的鏈接操做,要遠快於Buffer的鏈接操做。因此咱們在保存字符串的時候,該用string仍是要用string。那麼只有在保存非utf-8的字符串以及二進制數據的狀況,咱們才用Buffer

相關文章
相關標籤/搜索