一 Buffer概念
JavaScript在前端中處理字符串相關(guān)的API非常強(qiáng)大,好用,但是Node還需要額外處理網(wǎng)絡(luò)、文件中的一些二進(jìn)制數(shù)據(jù),ECMAScript標(biāo)準(zhǔn)中沒有提供這樣的API。
Buffer是Node提供的一個(gè)全新對(duì)象,非常類似JS中的Array,但是主要用來(lái)操作字節(jié)。Buffer在Node中由兩部分源碼實(shí)現(xiàn):
- Buffer/SlowBuffer:js核心模塊,主要用于實(shí)現(xiàn)Buffer在業(yè)務(wù)上的一些API
- node_buffer:C++內(nèi)建模塊,主要用于實(shí)現(xiàn)字節(jié)處理的性能相關(guān)部分
注意:Buffer由于在Node中使用場(chǎng)景非常廣泛,所以在Node進(jìn)程啟動(dòng)時(shí)就已經(jīng)加載,位于全局對(duì)象global中。
二 Buffer的基本使用
可以將Buffer理解為Node擴(kuò)充的數(shù)據(jù)類型,其作用類似Array,用于操作二進(jìn)制數(shù)據(jù)。
Buffer的創(chuàng)建:
// new Buffer() 的方式由于安全原因已經(jīng)過(guò)期,Node8之后推薦使用以下方式創(chuàng)建
let buf = Buffer.from('test','utf-8');
console.log(bf); // 輸出 <Buffer 74 65 73 74>
for(let i = 0; i < bf.length; i++){ //此length長(zhǎng)度和字符串的長(zhǎng)度有區(qū)別,指buffer的bytes大小
console.log(bf[i].toString(16)); // buffer[index]:獲取或設(shè)置在指定index索引未知的8位字節(jié)內(nèi)容
console.log(String.fromCharCode(bf[i])); //依次輸出 t e s t
console.log(bf.toString()); //輸出 test ,可選參數(shù)是 [encoding, start, end],默認(rèn)使用UTF-8
}
Node在文件、網(wǎng)絡(luò)操作中,如果沒有顯示聲明編碼格式,默認(rèn)返回的數(shù)據(jù)類型都是Buffer,比如readFile回調(diào)中的data。
注意:在ES6中增加了ArrayBuffer類型,Node中可以直接使用
Buffer的實(shí)例方法:
buf.write(string,[offset],[length],[encoding]):根據(jù)參數(shù)offset,將參數(shù)string數(shù)據(jù)寫入buffer
buf.toString([encoding],[length]):返回一個(gè)解碼的string類型
buf.toJSON():返回一個(gè)JSON表示的Buffer實(shí)例,JSON.stringify將會(huì)默認(rèn)調(diào)用來(lái)字符串序列化這個(gè)Buffer實(shí)例
buf.slice([start],[end]):返回一個(gè)新的buffer,這個(gè)buffer和老的buffer引用相同的內(nèi)存地址
buf.copy(targetBuffer,[targetStart],[sourceStart],[sourceEnd]):進(jìn)行buffer的拷貝,拷貝不會(huì)影響老的buffer。
Buffer的靜態(tài)方法:
Buffer.isBuffer(buf); // 判斷是不是Buffer
Buffer.byteLength(str); // 獲取字節(jié)長(zhǎng)度,第二個(gè)參數(shù)為字符集,默認(rèn)utf8
Buffer.concat(list[, totalLength]) // Buffer的拼接
三 Buffer的轉(zhuǎn)換
2.1 字符串轉(zhuǎn)Buffer
Buffer對(duì)象可以與字符串之間相互轉(zhuǎn)換,如下所示:
new Buffer(str, [encoding]); // 可選參數(shù)編碼格式若不傳入,則默認(rèn)按照UTF-8編碼進(jìn)行轉(zhuǎn)碼和存儲(chǔ)
一個(gè)Buffer對(duì)象可以存儲(chǔ)不同編碼類型的字符串轉(zhuǎn)碼的值,調(diào)用write()方法可以實(shí)現(xiàn)該目標(biāo),代碼如下:
buf.write(string, [offset], [length], [encoding]);
由于可以不斷寫入內(nèi)容到Buffer對(duì)象中,并且每次寫入可以指定編碼,所以Buffer對(duì)象中可以存在多種編碼轉(zhuǎn)換后的內(nèi)容,需要小心的是,每種編碼所用的字節(jié)長(zhǎng)度不同,將buffer反轉(zhuǎn)回字符串時(shí)需要謹(jǐn)慎處理。
2.2 Buffer轉(zhuǎn)字符串
buf.toString([encoding], [start], [end]);
2.3 Buffer不支持的編碼類型
Node的Buffer不支持中國(guó)的GBK,GB2312,BUG-5等編碼格式。判斷Buffer是否支持該編碼格式:
Buffer.isEncoding(encodibg); // 返回 true、false
對(duì)于不支持的編碼格式,Node有第三方模塊如 iconv 和 iconv-liten。
三 Buffer亂碼
3.1 亂碼的產(chǎn)生
在Buffer使用場(chǎng)景中,通常是以一段一段的方式傳輸,常見從輸入流中讀取內(nèi)容的示例如下:
var fs = require('fs');
var rs = fs.createReadStream('./demo.md');
var data = '';
rs.on("data", function(chunk) {
data += chunk;
});
rs.end("end", function(){
console.log(data);
});
上述代碼在讀取全英文格式內(nèi)容時(shí),不會(huì)有任何問(wèn)題,但是一旦輸入流中存在寬字節(jié)編碼,就會(huì)產(chǎn)生亂碼問(wèn)題。問(wèn)題來(lái)自于data += chunk,該句隱藏了 toString()操作,其內(nèi)部等價(jià)于:
data = data.toString() + chunk.toString();
下面模擬寬字節(jié)文字讀取場(chǎng)景:
var fs = require('fs');
var buf = Buffer.from("白銀之手騎士團(tuán)");
// <Buffer e7 99 bd e9 93 b6 e4 b9 8b e6 89 8b e9 aa 91 e5 a3 ab e5 9b a2 ef bc 81>
console.log("buf:", buf);
console.log("buf.length: ", buf.length); // 21
console.log("start: ", buf.toString("UTF-8", 0, 3)); // 白 e7 99 bd
console.log("start: ", buf.toString("UTF-8", 3, 6)); // 銀 e9 93 b6
console.log("start: ", buf.toString("UTF-8", 6, 9)); // 之 e4 b9 8b,e6 89 8b,e9 aa 91,e5 a3 ab,e5 9b a2,ef bc 81
var data = "";
var rs = fs.createReadStream("./demo.txt", {highWaterMark: 4});
rs.on("data", function(chunk) {
data += chunk;
});
rs.on("end", function(){
console.log("流式讀?。?, data); // 白?????手騎?????
});
在上述案例中,每3個(gè)長(zhǎng)度能夠讀取到一個(gè)漢字,但是在使用流式讀取時(shí),每4個(gè)長(zhǎng)度讀取一次,在第一讀取時(shí),就會(huì)讀取到多余的數(shù)據(jù)了,也即輸出了 白?,在第4次讀取時(shí),正好又讀取了原始數(shù)據(jù)的存儲(chǔ)要求,輸出了?手,依次類推。
3.2 亂碼解決
流式讀取可以設(shè)置編碼:
var rs = fs.createReadStream("./demo.txt", {highWaterMark: 4});
rs.setEncoding('utf8');
此時(shí)程序就能正常輸出數(shù)據(jù)!但是這并不是直接說(shuō)明了輸出沒有收到Buffer大小的影響。在實(shí)際運(yùn)行過(guò)程中,無(wú)論如何設(shè)置編碼,觸發(fā)的data事件次數(shù)都仍然是相同的。但是在每次data事件都會(huì)額外通過(guò)一個(gè)decoder對(duì)象對(duì)Buffer進(jìn)行轉(zhuǎn)換到字符串的解碼,然后傳遞給調(diào)用者。而這個(gè)decoder對(duì)象正是 setEncoding()方法時(shí)在可讀流對(duì)象內(nèi)部設(shè)置的。此時(shí)data收到的不再是原始的Buffer對(duì)象。decoder對(duì)象會(huì)被未轉(zhuǎn)碼的部分保留在StringDecode實(shí)例內(nèi)部,再下一次write的時(shí)候,會(huì)將上次的剩余字節(jié)和后續(xù)的新讀入的字節(jié)進(jìn)行組合!
setEncding只能解決UTF-8,Base64等帶來(lái)的編碼問(wèn)題,沒有從根本上解決問(wèn)題。正確的Buffer拼接方式應(yīng)該是用一個(gè)數(shù)組來(lái)存儲(chǔ)接收到的所有Buffer片段并記錄下所有的片段總長(zhǎng)度,然后調(diào)用Buffer.concat()方法生成一個(gè)合并的Buffer對(duì)象。
fs.createReadStream("./test.txt",{highWaterMark: 10});
var dataArr = [];
rs.on("data", function(chunk){
dataArr.push(chunk);
});
rs.on("end", function(){
var buf = Buffer.concat(dataArr);
console.log(buf.toString());
});
Buffer.concat()方法封裝了從小Buffer對(duì)象向大Buffer對(duì)象復(fù)制過(guò)程:
Buffer.concat = function(list, length) {
if (!Array.isArray(list)) {
throw new Error('Usage: Buffer.concat(list, [length])');
}
if (list.length === 0) {
return new Buffer(0);
} else if (list.length === 1) {
return list[0];
}
if (typeof length !== 'number') {
length = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
length += buf.length;
}
}
var buffer = new Buffer(length);
var pos = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}
return buffer;
};