Node.js 中 Buffer 對象的轉(zhuǎn)換及應用

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蝉绷、GB2312BIG-5編碼都不在支持的行列中鸭廷。

對于不支持的編碼類型,可以借助 Node 生態(tài)圈中的模塊完成轉(zhuǎn)換熔吗。iconviconv-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值的大小與讀取速度的關系:該值越大,讀取速度越快颜曾。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纠拔,一起剝皮案震驚了整個濱河市秉剑,隨后出現(xiàn)的幾起案子泛豪,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诡曙,死亡現(xiàn)場離奇詭異臀叙,居然都是意外死亡,警方通過查閱死者的電腦和手機价卤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門劝萤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人慎璧,你說我怎么就攤上這事床嫌。” “怎么了胸私?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵厌处,是天一觀的道長。 經(jīng)常有香客問我岁疼,道長阔涉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任捷绒,我火速辦了婚禮瑰排,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暖侨。我一直安慰自己椭住,他們只是感情好,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布它碎。 她就那樣靜靜地躺著函荣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扳肛。 梳的紋絲不亂的頭發(fā)上傻挂,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音挖息,去河邊找鬼金拒。 笑死,一個胖子當著我的面吹牛套腹,可吹牛的內(nèi)容都是我干的绪抛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼电禀,長吁一口氣:“原來是場噩夢啊……” “哼幢码!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起尖飞,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤症副,失蹤者是張志新(化名)和其女友劉穎店雅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贞铣,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡闹啦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辕坝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窍奋。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖酱畅,靈堂內(nèi)的尸體忽然破棺而出琳袄,到底是詐尸還是另有隱情,我是刑警寧澤纺酸,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布挚歧,位于F島的核電站,受9級特大地震影響吁峻,放射性物質(zhì)發(fā)生泄漏滑负。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一用含、第九天 我趴在偏房一處隱蔽的房頂上張望矮慕。 院中可真熱鬧,春花似錦啄骇、人聲如沸痴鳄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽痪寻。三九已至,卻和暖如春虽惭,著一層夾襖步出監(jiān)牢的瞬間橡类,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工芽唇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留顾画,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓匆笤,卻偏偏與公主長得像研侣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子炮捧,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 編碼問題一直困擾著開發(fā)人員庶诡,尤其在 Java 中更加明顯,因為 Java 是跨平臺語言咆课,不同平臺之間編碼之間的切換...
    x360閱讀 2,465評論 1 20
  • 在NODE中末誓,應用需要處理網(wǎng)絡協(xié)議璧函、操作系統(tǒng)數(shù)據(jù)庫、處理圖片基显、接受上傳文件等,在網(wǎng)絡流和文件的操作中善炫,需要處理大量...
    TaoGeNet閱讀 2,080評論 0 2
  • 為什么要編碼 不知道大家有沒有想過一個問題撩幽,那就是為什么要編碼?我們能不能不編碼箩艺?要回答這個問題必須要回到計算機是...
    艾小天兒閱讀 17,260評論 0 2
  • Node.js Buffer(緩沖區(qū)) JavaScript 語言自身只有字符串數(shù)據(jù)類型窜醉,沒有二進制數(shù)據(jù)類型。但在...
    FTOLsXD閱讀 494評論 0 2
  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,305評論 0 6