【深入探究Node】(5)“Buffer與亂碼的故事” 有十問(wèn)
1. 為什么要有Buffer對(duì)象?
2. 可以談?wù)勀闼J(rèn)識(shí)的Buffer對(duì)象嗎?
模塊結(jié)構(gòu)
Buffer對(duì)象結(jié)構(gòu)
3. 哇塞,原來(lái)Buffer對(duì)象這么有意思,還可以當(dāng)成Array來(lái)使用,我突發(fā)奇想,要是給元素賦值的值是小數(shù)而不是整數(shù)會(huì)怎么樣呢?
4. 我看Buffer對(duì)象很像字符串,它兩可以互轉(zhuǎn)嗎?
字符串轉(zhuǎn)Buffer
Buffer轉(zhuǎn)字符串
5. Buffer應(yīng)該是常見(jiàn)于輸入輸入流中,你可以說(shuō)說(shuō)怎么使用嗎?
6. 我有時(shí)候這樣讀取數(shù)據(jù),然后打印出來(lái),有時(shí)候會(huì)出現(xiàn)亂碼,是什么原因呢?
7.為什么 “月”、“是”、“望”、“低”4個(gè)字沒(méi)有被正常輸出,取而代之的是3個(gè)亂碼?
8. so噶!那樣的話,那我限制Buffer對(duì)象的長(zhǎng)度為12,就不會(huì)有問(wèn)題了吧!但是這樣每次都要數(shù),很麻煩,有沒(méi)有簡(jiǎn)單的方法呢?
9. 哇塞,真是令人興奮,Node是如何實(shí)現(xiàn)這個(gè)輸出結(jié)果的呢?
10. 可是設(shè)置decoder后,即使被轉(zhuǎn)碼,那也無(wú)法改變寬字節(jié)字符串被截?cái)嗟膯?wèn)題???
1. 為什么要有Buffer對(duì)象?
在Node中,應(yīng)用需要處理網(wǎng)絡(luò)協(xié)議、操作數(shù)據(jù)庫(kù)、處理圖片、接收上傳文件等,在網(wǎng)絡(luò)流和文件的操作中,還要處理大量二進(jìn)制數(shù)據(jù),JavaScript自有的字符串遠(yuǎn)遠(yuǎn)不能滿足這些需求,于是Buffer對(duì)象應(yīng)運(yùn)而生。
Buffer在文件I/O和網(wǎng)絡(luò)I/O中運(yùn)用廣泛,尤其在網(wǎng)絡(luò)傳輸中,它的性能舉足輕重。在應(yīng)用中,我們通常會(huì)操作字符串,但一旦在網(wǎng)絡(luò)中傳輸,都需要轉(zhuǎn)換為Buffer,以進(jìn)行二進(jìn)制數(shù)據(jù)傳輸。在Web應(yīng)用中,字符串轉(zhuǎn)換到Buffer是時(shí)時(shí)刻刻發(fā)生的,提高字符串到Buffer的轉(zhuǎn)換效率,可以很大程度地提高網(wǎng)絡(luò)吞吐率。
2. 可以談?wù)勀闼J(rèn)識(shí)的Buffer對(duì)象嗎?
嗯嗯,好的。
Buffer是一個(gè)像Array的對(duì)象,但它主要用于操作字節(jié)。所以我將會(huì)從模塊結(jié)構(gòu)和對(duì)象結(jié)構(gòu)的層面上來(lái)認(rèn)識(shí)它。
模塊結(jié)構(gòu)
Buffer是一個(gè)典型的JavaScript與C++結(jié)合的模塊,它將性能相關(guān)部分用C++實(shí)現(xiàn),將非性能相關(guān)的部分用JavaScript實(shí)現(xiàn),如圖所示。
在【深入探究Node】(4)“內(nèi)存控制” 有十五問(wèn)我們提到Buffer所占用的內(nèi)存不是通過(guò)V8分配的,屬于堆外內(nèi)存。由于V8垃圾回收性能的影響,將常用的操作對(duì)象用更高效和專有的內(nèi)存分配回收策略來(lái)管理是個(gè)不錯(cuò)的思路。由于Buffer太過(guò)常見(jiàn),Node在進(jìn)程啟動(dòng)時(shí)就已經(jīng)加載了它,并將其放在全局對(duì)象(global)上。所以在使用Buffer時(shí),無(wú)須通過(guò)require()即可直接使用。
Buffer對(duì)象結(jié)構(gòu)
Buffer對(duì)象類似于數(shù)組,它的元素為16進(jìn)制的兩位數(shù),即0到255的數(shù)值。示例代碼如下所示:
由上面的示例可見(jiàn),不同編碼的字符串占用的元素個(gè)數(shù)各不相同,上面代碼中的中文字在UTF-8編碼下占用3個(gè)元素,字母和半角標(biāo)點(diǎn)符號(hào)占用1個(gè)元素。
Buffer受Array類型的影響很大,可以訪問(wèn)length屬性得到長(zhǎng)度,也可以通過(guò)下標(biāo)訪問(wèn)元素,在構(gòu)造對(duì)象時(shí)也十分相似,代碼如下:
上述代碼分配了一個(gè)長(zhǎng)100字節(jié)的Buffer對(duì)象??梢酝ㄟ^(guò)下標(biāo)訪問(wèn)剛初始化的Buffer的元素,代碼如下:
這里會(huì)得到一個(gè)比較奇怪的結(jié)果,它的元素值是一個(gè)0到255的隨機(jī)值。同樣,我們也可以通過(guò)下標(biāo)對(duì)它進(jìn)行賦值:
3. 哇塞,原來(lái)Buffer對(duì)象這么有意思,還可以當(dāng)成Array來(lái)使用,我突發(fā)奇想,要是給元素賦值的值是小數(shù)而不是整數(shù)會(huì)怎么樣呢?
給元素的賦值如果小于0,就將該值逐次加256,直到得到一個(gè)0到255之間的整數(shù)。如果得到的數(shù)值大于255,就逐次減256,直到得到0~255區(qū)間內(nèi)的數(shù)值。如果是小數(shù),舍棄小數(shù)部分,只保留整數(shù)部分。
4. 我看Buffer對(duì)象很像字符串,它兩可以互轉(zhuǎn)嗎?
可以的。
字符串轉(zhuǎn)Buffer
字符串轉(zhuǎn)Buffer對(duì)象主要是通過(guò)構(gòu)造函數(shù)完成的:
通過(guò)構(gòu)造函數(shù)轉(zhuǎn)換的Buffer對(duì)象,存儲(chǔ)的只能是一種編碼類型。encoding參數(shù)不傳遞時(shí),默認(rèn)按UTF-8編碼進(jìn)行轉(zhuǎn)碼和存儲(chǔ)。
Buffer轉(zhuǎn)字符串
實(shí)現(xiàn)Buffer向字符串的轉(zhuǎn)換也十分簡(jiǎn)單,Buffer對(duì)象的toString()可以將Buffer對(duì)象轉(zhuǎn)換為字符串,代碼如下:
比較精巧的是,可以設(shè)置encoding(默認(rèn)為UTF-8)、start、end這3個(gè)參數(shù)實(shí)現(xiàn)整體或局部的轉(zhuǎn)換。如果Buffer對(duì)象由多種編碼寫(xiě)入,就需要在局部指定不同的編碼,才能轉(zhuǎn)換回正常的編碼。
5. Buffer應(yīng)該是常見(jiàn)于輸入輸入流中,你可以說(shuō)說(shuō)怎么使用嗎?
Buffer在使用場(chǎng)景中,通常是以一段一段的方式傳輸。以下是常見(jiàn)的從輸入流中讀取內(nèi)容的示例代碼:
上面這段代碼常見(jiàn)于國(guó)外,用于流讀取的示范,data事件中獲取的chunk對(duì)象即是Buffer對(duì)象。對(duì)于初學(xué)者而言,容易將Buffer當(dāng)做字符串來(lái)理解,所以在接受上面的示例時(shí)不會(huì)覺(jué)得有任何異常。
6. 我有時(shí)候這樣讀取數(shù)據(jù),然后打印出來(lái),有時(shí)候會(huì)出現(xiàn)亂碼,是什么原因呢?
一旦輸入流中有寬字節(jié)編碼時(shí),問(wèn)題就會(huì)暴露出來(lái)。如果你在通過(guò)Node開(kāi)發(fā)的網(wǎng)站上看到[插圖]亂碼符號(hào),那么該問(wèn)題的起源多半來(lái)自于這里。
“用多個(gè)字節(jié)來(lái)代表的字符稱之為寬字符,而Unicode只是寬字符編碼的一種實(shí)現(xiàn),寬字符并不一定是Unicode。
”
這里潛藏的問(wèn)題在于如下這句代碼:
這句代碼里隱藏了toString()操作,它等價(jià)于如下的代碼:
值得注意的是,外國(guó)人的語(yǔ)境通常是指英文環(huán)境,在他們的場(chǎng)景下,這個(gè)toString()不會(huì)造成任何問(wèn)題。但對(duì)于寬字節(jié)的中文,卻會(huì)形成問(wèn)題。為了重現(xiàn)這個(gè)問(wèn)題,下面我們模擬近似的場(chǎng)景,將文件可讀流的每次讀取的Buffer長(zhǎng)度限制為11,代碼如下:
搭配該代碼的測(cè)試數(shù)據(jù)為李白的《靜夜思》。執(zhí)行該程序,將會(huì)得到以下輸出:
7.為什么 “月”、“是”、“望”、“低”4個(gè)字沒(méi)有被正常輸出,取而代之的是3個(gè)亂碼?
產(chǎn)生這個(gè)輸出結(jié)果的原因在于文件可讀流在讀取時(shí)會(huì)逐個(gè)讀取Buffer。
這首詩(shī)的原始Buffer應(yīng)存儲(chǔ)為:
由于我們限定了Buffer對(duì)象的長(zhǎng)度為11,因此只讀流需要讀取7次才能完成完整的讀取,結(jié)果是以下幾個(gè)Buffer對(duì)象依次輸出:
上文提到的buf.toString()方法默認(rèn)以UTF-8為編碼,中文字在UTF-8下占3個(gè)字節(jié)。所以第一個(gè)Buffer對(duì)象在輸出時(shí),只能顯示3個(gè)字符,Buffer中剩下的2個(gè)字節(jié)(e6 9c)將會(huì)以亂碼的形式顯示。第二個(gè)Buffer對(duì)象的第一個(gè)字節(jié)也不能形成文字,只能顯示亂碼。于是形成一些文字無(wú)法正常顯示的問(wèn)題。
在這個(gè)示例中我們構(gòu)造了11這個(gè)限制,但是對(duì)于任意長(zhǎng)度的Buffer而言,寬字節(jié)字符串都有可能存在被截?cái)嗟那闆r,只不過(guò)Buffer的長(zhǎng)度越大出現(xiàn)的概率越低而已,但該問(wèn)題依然不可忽視。
8. so噶!那樣的話,那我限制Buffer對(duì)象的長(zhǎng)度為12,就不會(huì)有問(wèn)題了吧!但是這樣每次都要數(shù),很麻煩,有沒(méi)有簡(jiǎn)單的方法呢?
有的,我們別忘了可讀流還有一個(gè)設(shè)置編碼的方法setEncoding(),示例如下:
該方法的作用是讓data事件中傳遞的不再是一個(gè)Buffer對(duì)象,而是編碼后的字符串。為此,我們繼續(xù)改進(jìn)前面詩(shī)歌的程序,添加setEncoding()的步驟如下:
重新執(zhí)行程序,得到輸出:


9. 哇塞,真是令人興奮,Node是如何實(shí)現(xiàn)這個(gè)輸出結(jié)果的呢?
事實(shí)上,在調(diào)用setEncoding()時(shí),可讀流對(duì)象在內(nèi)部設(shè)置了一個(gè)decoder對(duì)象。每次data事件都通過(guò)該decoder對(duì)象進(jìn)行Buffer到字符串的解碼,然后傳遞給調(diào)用者。是故設(shè)置編碼后,data不再收到原始的Buffer對(duì)象。
10. 可是設(shè)置decoder后,即使被轉(zhuǎn)碼,那也無(wú)法改變寬字節(jié)字符串被截?cái)嗟膯?wèn)題?。?/span>
decoder對(duì)象來(lái)自于string_decoder模塊StringDecoder的實(shí)例對(duì)象。
可以看看 下面的代碼:
我將前文提到的前兩個(gè)Buffer對(duì)象寫(xiě)入decoder中。奇怪的地方在于“月”的轉(zhuǎn)碼并沒(méi)有如平常一樣在兩個(gè)部分分開(kāi)輸出。StringDecoder在得到編碼后,知道寬字節(jié)字符串在UTF-8編碼下是以3個(gè)字節(jié)的方式存儲(chǔ)的,所以第一次write()時(shí),只輸出前9個(gè)字節(jié)轉(zhuǎn)碼形成的字符,“月”字的前兩個(gè)字節(jié)被保留在StringDecoder實(shí)例內(nèi)部。第二次write()時(shí),會(huì)將這2個(gè)剩余字節(jié)和后續(xù)11個(gè)字節(jié)組合在一起,再次用3的整數(shù)倍字節(jié)進(jìn)行轉(zhuǎn)碼。于是亂碼問(wèn)題通過(guò)這種中間形式被解決了。
“公眾號(hào)《前端陽(yáng)光》,回復(fù) 加群,可以加入技術(shù)交流群哦!
”







