Buffer 對象可以與字符串之間相互轉(zhuǎn)換。目前支持的字符串編碼類型有:
- ASCII
- UTF-8
- UTF-16LE/UCS-2
- Base64
- Binary
- Hex
Buffer 與字符串之間的轉(zhuǎn)換
字符串轉(zhuǎn) Buffer 對象主要是通過構(gòu)造函數(shù)完成的:
new Buffer(str, [encoding]);
通過構(gòu)造函數(shù)轉(zhuǎn)換的 Buffer 對象,存儲的只能是一種編碼類型。默認按 UTF-8 編碼進行轉(zhuǎn)碼和存儲媒殉。
一個 Buffer 對象可以存儲不同編碼類型的字符串轉(zhuǎn)碼的值,調(diào)用write()
方法可以實現(xiàn)該目的:
buf.write(string, [offset], [length], [encoding]);
由于可以不斷寫入內(nèi)容到 Buffer 對象中,并且每次寫入可以指定編碼盅粪,所以 Buffer 對象中可以存在多種編碼轉(zhuǎn)化后的內(nèi)容。但是由于每種編碼所用的字節(jié)長度不同悄蕾,將 Buffer 反轉(zhuǎn)回字符串時需要謹慎處理票顾。
Buffer 對象轉(zhuǎn)字符串是通過 toString()
方法:
buf.toString([encoding], [start], [end]);
base64 編碼與解碼
// 將字符串轉(zhuǎn)化為 base64 編碼
let str = new Buffer('key1=value1&key2=value2').toString('base64');
console.log(str); // 'a2V5MT12YWx1ZTEma2V5Mj12YWx1ZTI='
// 解碼
console.log(new Buffer(str, 'base64').toString()); // 'key1=value1&key2=value2'
應用:編碼解碼圖片
let fs = require('fs');
// function to encode file data to base64 encoded string
function base64_encode(file) {
// read binary data
let bitmap = fs.readFileSync(file);
// convert binary data to base64 encoded string
return new Buffer(bitmap).toString('base64');
}
// function to create file from base64 encoded string
function base64_decode(base64str, file) {
// create buffer object from base64 encoded string, it is important to tell the constructor that the string is base64 encoded
let bitmap = new Buffer(base64str, 'base64');
// write buffer to file
fs.writeFileSync(file, bitmap);
console.log('******** File created from base64 encoded string ********');
}
// convert image to base64 encoded string
let base64str = base64_encode('kitten.jpg');
console.log(base64str);
// convert base64 string back to image
base64_decode(base64str, 'copy.jpg');
iconv
目前比較遺憾的是,Node 和 Buffer 對象支持的編碼類型有限帆调,只有少數(shù)的幾種編碼類型可以在字符串和 Buffer 之間轉(zhuǎn)換奠骄。為此,Buffer 提供了一個isEncoding()
函數(shù)來判斷編碼是否支持轉(zhuǎn)換:
Buffer.isEncoding(encoding)
將編碼類型作為參數(shù)傳入上面的函數(shù)番刊,如果支持轉(zhuǎn)換則返回值為true
含鳞,否則為false
。遺憾的是芹务,在中國常用的GBK
蝉绷、GB2312
和BIG-5
編碼都不在支持的行列中鸭廷。
對于不支持的編碼類型,可以借助 Node 生態(tài)圈中的模塊完成轉(zhuǎn)換熔吗。iconv
和iconv-lite
兩個模塊可以支持更多的編碼類型轉(zhuǎn)換辆床,包括寬字節(jié)編碼 GBK 和 GB2312。
iconv-lite
采用純Javascript實現(xiàn)桅狠,iconv
則通過 C++ 調(diào)用libiconv
庫完成讼载。前者比后者輕量,無須編譯和處理環(huán)境依賴直接使用垂攘。在性能方面维雇,由于轉(zhuǎn)碼都是耗用 CPU,在 V8 的高性能下晒他,少了 C++ 到 Javascript 的層次轉(zhuǎn)換吱型,純 Javascript 的性能比 C++ 實現(xiàn)得更好。
以下為iconv-lite
的實例代碼:
let iconv = require('iconv-lite');
// Buffer 轉(zhuǎn)字符串
let str = iconv.decode(buf, 'GBK');
// 字符串轉(zhuǎn) Buffer
let buf = iconv.encode(str, 'GBK');
Buffer 的拼接
Buffer 在使用場景中陨仅,通常是以一段一段的方式傳輸津滞。以下是常見的從輸入流讀取內(nèi)容的實例代碼:
let fs = require('fs');
let rs = fs.createReadStream('test.md');
let data = '';
rs.on('data', function (chunk) {
data += chunk;
});
rs.on('end', function () {
console.log(data);
});
上面這段代碼常見與國外,用于流讀取的示范灼伤,data 事件中獲取的chunk
對象即是 Buffer 對象触徐。對于初學者而言,容易將 Buffer 當作字符串來理解狐赡,所以在接受上面的示例時不會覺得有任何異常撞鹉。
一旦輸入流中有寬字節(jié)編碼時,問題就會暴露出來颖侄。如果你在通過 Node 開發(fā)的網(wǎng)站上看到?亂碼符號鸟雏,那么該問題的起源多半來自于這里。
這里潛藏的問題在于如下這句代碼:
data += chunk;
這句代碼里隱藏了toString()
操作览祖,它等價于如下代碼:
data = data.toString() + chunk.toString();
值得注意的是孝鹊,外國人的語境通常是英文環(huán)境,在他們的場景下展蒂,這個toString()
不會造成任何問題又活。但對于寬字節(jié)的中文,卻會形成問題锰悼。比如下面將文件可讀流每次讀取的 Buffer 長度限制為 11:
let rs = fs.createReadStream('test.md', {highWaterMark: 11});
將會得到以下輸出:
床前明???光柳骄,疑???地上霜;舉頭???明月箕般,???頭思故鄉(xiāng)夹界。
亂碼的產(chǎn)生
產(chǎn)生這個輸出結(jié)果的原因在于文件可讀流在讀取時會逐個讀取 Buffer。由于限定了 Buffer 的長度為 11,因此只讀流需要讀取 7 次才能完成完整的讀取可柿,結(jié)果是以下幾個 Buffer 對象的依次輸出:
<Buffer e5 ba 8a e5 89 8d e6 98 8e e6 9c>
<Buffer 88 e5 85 89 ef bc 8c e7 96 91 e6>
<Buffer 98 af e5 9c b0 e4 b8 8a e9 9c 9c>
<Buffer ef bc 9b e4 b8 be e5 a4 b4 e6 9c>
<Buffer 9b e6 98 8e e6 9c 88 ef bc 8c e4>
<Buffer bd 8e e5 a4 b4 e6 80 9d e6 95 85>
<Buffer e4 b9 a1 e3 80 82>
上文提到buf.toString()
方法默認以 UTF-8 為編碼鸠踪,中文字在 UTF-8 下占 3 個字節(jié)。所以第一個 Buffer 對象在輸出時复斥,只能顯示 3 個字符营密,Buffer 中剩下的 2 個字節(jié)(e6 9c)將會以亂碼的形式顯示。第二個 Buffer 對象的第一個字節(jié)也不能形成文字目锭,只能顯示亂碼评汰。于是形成一些文字無法正常顯示的問題。
這個示例中我們構(gòu)造了 11 這個限制痢虹,但是對于任意長度的 Buffer 而言被去,寬字節(jié)字符串都有可能存在被截斷的情況,只不過 Buffer 的長度越大出現(xiàn)的概率越低而已奖唯。
setEncoding() 與 string_decoder()
可讀流有一個設置編碼的方法setEncoding()
:
readable.setEncoding(encoding);
該方法的作用是讓data
事件中傳遞的不再是一個 Buffer 對象惨缆,而是編碼后的字符串。
let rs = fs.createReadStream('test.md', {highWaterMark: 11});
rs.setEncoding('utf8');
重新執(zhí)行程序丰捷,將輸出:
床前明月光坯墨,疑是地上霜;舉頭望明月病往,低頭思故鄉(xiāng)捣染。
但是無論如何設置編碼,觸發(fā)data
事件的次數(shù)依舊相同停巷,這意味著設置編碼并不能改變按段讀取的基本方式耍攘。
事實上,在調(diào)用setEncoding()
時畔勤,可讀流對象在內(nèi)部設置了一個decoder
對象蕾各。每次data
事件都通過該decoder
對象進行 Buffer 到字符串的編碼,然后傳遞給調(diào)用者硼被。所以設置編碼后,data
不再收到原始的 Buffer 對象渗磅。但是這依舊無法解釋為何設置編碼后亂碼問題被解決了嚷硫,因為在前述分析中,無論如何轉(zhuǎn)碼始鱼,總是存在寬字節(jié)字符串被截斷的問題仔掸。
最終亂碼問題得以解決,還是在于decoder
的神奇之處医清。decoder
對象來自于string_decoder
模塊StringDecoder
的實例對象:
let StringDecoder = require('string_decoder').StringDecoder;
let decoder = new StringDecoder('utf8'); // 床前明
let buf1 = new Buffer([0xE5, 0xBA, 0x8A, 0xE5, 0x89, 0x8D, 0xE6, 0x98, 0x8E, 0xE6, 0x9C]);
console.log(decoder.write(buf1));
let buf2 = new Buffer([0x88, 0xE5, 0x85, 0x89, 0xEF, 0xBC, 0x8C, 0xE7, 0x96, 0x91, 0xE6]);
console.log(decoder.write(buf2)); // 月光起暮,疑
將前文提到的前兩個 Buffer 對象寫入decoder
中。奇怪的地方在于“月”的轉(zhuǎn)碼并沒有如平常一樣在兩個部分分開輸出会烙。StringDecoder
在得到編碼后负懦,知道寬字節(jié)字符串在 UTF-8 編碼下是以 3 個字節(jié)的方式存儲的筒捺,所以第一次輸出時,只輸出前 9 個字節(jié)轉(zhuǎn)碼形成的字符纸厉,“月”字的前兩個字節(jié)被保留在StringDecoder
實例內(nèi)部系吭。第二次輸出時,會將這 2 個剩余字節(jié)和后續(xù) 11 個字節(jié)組合在一起颗品,再次用 3 的整數(shù)倍字節(jié)進行轉(zhuǎn)碼肯尺。于是亂碼往年提通過這種中間形式被解決了。
雖然string_decoder
模塊很奇妙躯枢,但是并非萬能则吟,目前只能處理 UTF-8、Base64 和 UCS-2锄蹂、UTF-16LE 這 3 種編碼氓仲。所以,通過string_decoder
不可否認能解決大部分亂碼問題败匹,但并不能從根本上解決問題寨昙。
正確地拼接 Buffer
正確地解決亂碼的方式是將多個小 Buffer 對象拼接為一個 Buffer 對象,然后通過iconv-lite
一類的模塊來轉(zhuǎn)碼掀亩。正確的 Buffer 拼接方法如下:
let chunks = [];
let size = 0;
res.on('data', function (chunk) {
chunks.push(chunk);
size += chunk.length;
});
res.on('end', function () {
let buf = Buffer.concat(chunks, size);
let str = iconv.decode(buf, 'utf8');
console.log(str);
});
正確的拼接方式是用一個數(shù)組來存儲接收到的所有 Buffer 判斷并記錄下所有片段的總長度舔哪,然后調(diào)用Buffer.concat()
方法封裝了從小 Buffer 對象向大 Buffer 對象的復制過程。
Buffer.concat()
方法實現(xiàn)如下:
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 (let i = 0; i < list.length; i ++) {
let buf = list[i];
length += buf.length;
}
}
let buffer = new Buffer(length);
let pos = 0;
for (let i = 0; i < list.length; i ++) {
let buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}
return buffer;
};
Buffer 與傳輸性能的關系
網(wǎng)絡 I/O
在應用中槽棍,我們通常會操作字符串捉蚤,但一旦在網(wǎng)絡中傳輸,都需要轉(zhuǎn)換為 Buffer炼七,以進行二進制數(shù)據(jù)傳輸缆巧。
let http = require('http');
let str = '';
for (let i = 0; i < 1024 * 10; i ++) {
str += 'a';
}
let buf = new Buffer(str);
http.createServer(function (req, res) {
res.writeHead(200);
res.end(buf);
}).listen(8001);
通過預先轉(zhuǎn)換靜態(tài)內(nèi)容為 Buffer 對象,使向客戶端輸出的是一個 Buffer 對象豌拙,無需在每次響應時進行轉(zhuǎn)換陕悬,可以有效減少 CPU 的重復使用,節(jié)省服務器資源按傅,提高網(wǎng)絡吞吐率捉超。
所以在不需要改變內(nèi)容的場景下,盡量只讀取 Buffer唯绍,然后直接傳輸拼岳,不做額外的轉(zhuǎn)換,避免消耗况芒。在 Node 構(gòu)建的 Web 應用中惜纸,可以選擇將頁面中的動態(tài)內(nèi)容和靜態(tài)內(nèi)容分離,靜態(tài)內(nèi)容部分可以預先轉(zhuǎn)換為 Buffer。
文件 I/O
fs.createReadStream()
的工作方式是在內(nèi)存中準備一段 Buffer耐版,然后在fs.read()
讀取時逐步從磁盤中將字節(jié)復制到 Buffer 中祠够。完成一次讀取時,則從這個 Buffer 中通過slice()
方法取出部分數(shù)據(jù)作為一個小 Buffer 對象椭更,再通過data
事件傳遞給調(diào)用方哪审。如果 Buffer 用完,則重新分配一個虑瀑;如果還有剩余湿滓,則繼續(xù)使用。分配一個新的 Buffer 對象的操作如下:
let pool;
function allocNewPool(poolSize) {
pool = new Buffer(poolSize);
pool.used = 0;
}
在理想的狀況下舌狗,每次讀取的長度就是用戶指定的highWaterMark
叽奥。但是有可能讀到了文件結(jié)尾,或者文件本身就沒有指定的highWaterMark
那么大痛侍,這個預先指定的 Buffer 對象將會有部分剩余朝氓,不過好在這里的內(nèi)存可以分配給下次讀取時使用。pool
是常駐內(nèi)存的主届,只有當pool
單元剩余數(shù)量小于 128 (kMinPoolSpace)字節(jié)時赵哲,才會重新分配一個新的 Buffer 對象。分配新的 Buffer 對象的判斷條件如下:
if (!pool || pool.length - pool.used < kMinPoolSpace) {
// discard the old pool
pool = null;
allocNewPool(this._readableState.highWaterMark);
}
這里與 Buffer 的內(nèi)存分配類似君丁,highWaterMark
大小對性能有兩個影響的點枫夺。
-
highWaterMark
設置對 Buffer 內(nèi)存的分配和使用有一定影響 -
highWaterMark
設置過小,可能導致系統(tǒng)調(diào)用次數(shù)過多
文件流讀取基于 Buffer 分配绘闷,Buffer 則基于 SlowBuffer 分配橡庞,這可以理解為兩個維度的分配策略。如果文件較杏≌帷(< 8KB)扒最,可能造成 slab 未能完全使用。
由于fs.createReadStream()
內(nèi)部采用fs.read()
實現(xiàn)华嘹,將會引起對磁盤的系統(tǒng)調(diào)用吧趣,對大文件而言,highWaterMark
的大小會決定觸發(fā)系統(tǒng)調(diào)用和data
事件的次數(shù)耙厚。讀取一個相同的大文件時强挫,highWaterMark
值的大小與讀取速度的關系:該值越大,讀取速度越快颜曾。