這篇文章帶大家深入了解下Node中 Buffer(緩衝區)類,希望對大家有幫助!
在TypedArray出來之前,JavaScript這門語言是無法很好地處理原始二進位資料(raw binary data)的,這是因為一開始的時候JavaScript主要還是應用在瀏覽器中作為腳本語言使用,所以需要處理原生二進位資料的場景是少之又少。而Node出來後,由於服務端的應用需要處理大量的二進位流例如檔讀寫,TCP連接等,所以Node在JavaScript(V8)之外,定義了一種新的資料型別Buffer。由於Buffer在Node應用中使用十分廣泛,所以只有真正掌握了它的用法,你才能寫出更好的Node應用。 【相關教學推薦:nodejs影片教學、程式設計教學】
Buffer的具體用法之前,我們先來簡單回顧一下有關二元的知識。
電腦所有的資料底層都是以二進位(binary)的格式儲存的。換句話說你電腦裡面的文件,不管是純文字還是圖片還是視頻,在電腦的硬碟裡面都是由01這兩個數字組成的。在電腦科學中我們把0或1單一數字叫做一個位元(bit),8個位元可以組成一個位元組(byte)。十進制(decimal)數字16如果用1個位元組來表示的話,底層儲存結構是:我們可以看到16用二進位表示的話相比於十進制的表示一下子多了6位數字,如果數字再大點的話二進制的位數會更多,這樣我們無論是閱讀還是寫起來都很不方便。因為這個原因,程式設計師一般喜歡用十六進位(hexadecimal)來表示資料而不是直接使用二進制,例如我們在寫CSS的時候color的值用的就是16進位(例如#FFFFFF)而不是一堆0和1。
字元編碼
既然所有資料底層都是二進制,網路傳輸的資料也是二進位的話,為什麼我們現在閱讀的文章是中文而不是一堆0和1呢?這裡就要介紹一下字元編碼的概念了。所謂的字元編碼簡單來說就是一個映射關係表,它表示的是字元(中文字元、英文字元或者其它字元)是如何和二進位數字(包含若干個位元組)對應起來的。舉個例子,如果使用我們熟悉的ascii來編碼,a這個英文字元的二進位表示是0b01100001(0b是二進位數字的前綴)。因此當我們的電腦從某個以ascii編碼的檔案中讀取到0b01100001這串二進位資料時,就會在螢幕中顯示a這個字符,同樣a這個字元保存到電腦中或在網路上傳輸都是0b01100001這個二進位資料。除了ascii碼,常見的字元編碼還有utf-8和utf-16等。
二元知識和字元編碼的概念後,我們終於可以正式學習Buffer了。我們先來看看Buffer的官方定義:
TheBuffer
########################################################################################################################都class in Node.js is designed to handle raw binary data. Each buffer corresponds to some raw memory allocated outside V8. Buffers act somewhat like arrays of integers, but aren't resizable and have a whole bunch of methods specifically for binary data. The integers in a ufferd perrespem bufferm from 0 to 255 inclusive. When using
console.log()to print the
Bufferinstance, you'll get a chain of values in hexadecimal values.
簡單來說所謂的Buffer就是Node在V8堆記憶體之外分配的一塊固定大小的記憶體空間。當Buffer被用console.log列印出來時,會以位元組為單位,印出一串以十六進位表示的值。
建立Buffer
了解完Buffer的基本概念後,我們再來建立一個Buffer對象。創建Buffer的方式有很多種,常見的有Buffer.alloc,Buffer.allocUnsafe和Buffer.from。
這是最常見的創建Buffer的方式,只需要傳入Buffer的大小即可
const buff = Buffer.alloc(5) console.log(buff) // Prints: <Buffer 00 00 00 00 00>
上面的程式碼中我建立了一個大小為5個位元組的Buffer區域,console.log函數會印出五個連續的十六進位數字,表示目前Buffer儲存的內容。我們可以看到目前的Buffer被填滿了0,這是Node預設的行為,我們可以設定後面兩個參數fill和encoding來指定初始化的時候填入另外的內容。
這裡值得一提的是我在上面的程式碼中使用的是Node全域的Buffer對象,而沒有從node:buffer套件中明確導入,這完全是因為編寫方便,在實際開發中應該採用後者的寫法:
import { Buffer } from 'node:buffer'
#Buffer.allocUnsafe和Buffer.alloc的最大差異是使用allocUnsafe函數申請到的記憶體空間是沒有被初始化的,也是說可能還殘留了上次使用的數據,因此會有資料安全的問題。 allocUnsafe函數接收一個size參數作為buffer區域的大小:
const buff = Buffer.allocUnsafe(5) console.log(buff) // Prints (实际内容可能有出入): <Buffer 8b 3f 01 00 00>
從上面的輸出結果來看我們是控制不了使用Buffer.allocUnsafe分配出來的buffer內容的。也正是由於不對分配過來的記憶體進行初始化所以這個函數分配Buffer的速度會比Buffer.alloc更快,我們在實際開發中應該根據自己實際的需要進行取捨。
這個函數是我們最常使用的建立Buffer的函數,它有許多不同的重載,也就是說傳入不同的參數會有不同的表現行為。我們來看幾個常見的重載:
當我們傳入的第一個參數是字串類型時,Buffer.from會根據字串的編碼(encoding參數,預設是utf8)產生該字串對應的二進位表示。看個例子:
const buff = Buffer.from('你好世界') console.log(buff) // Prints: <Buffer e4 bd a0 e5 a5 bd e4 b8 96 e7 95 8c> console.log(buff.toString()) // Prints: '你好世界' console.log(buff.toString('ascii')) // Prints: ''d= e%=d8\x16g\x15\f''
在上面例子中,我使用"你好世界"這個字串完成了Buffer的初始化工作,由於我沒有傳入第二個encoding參數,所以預設使用的是utf8編碼。後面我們透過查看第一個console.log的輸出可以發現,雖然我們傳入的字串只有四個字符,可是初始化的Buffer卻有12個字節,這是因為utf8編碼中一個漢字會使用3個位元組來表示。接著我們透過buff.toString() 方法來查看buff的內容,由於toString方法的預設編碼輸出格式是utf8,所以我們可以看到第二個console .log可以正確輸出buff儲存的內容。不過在第三個console.log中我們指定了字元的編碼型別是ascii,這時候我們會看到一堆亂碼。看到這裡我想你對我之前提到的字元編碼一定有更深的認識了。
當Buffer.from接收的參數是一個buffer物件時,Node會建立一個新的Buffer實例,然後將傳進來的buffer內容拷貝到新的Buffer物件裡面。
const buf1 = Buffer.from('buffer') const buf2 = Buffer.from(buf1) console.log(buf1) // Prints: <Buffer 62 75 66 66 65 72> console.log(buf2) // Prints: <Buffer 62 75 66 66 65 72> buf1[0] = 0x61 console.log(buf1.toString()) // Prints: auffer console.log(buf2.toString()) // Prints: buffer
在上面的例子中,我們先建立了一個Buffer物件buf1,裡面儲存的內容是"buffer"這個字串,然後透過這個Buffer物件初始化了一個新的Buffer物件buf2。這時候我們將buf1的第一個位元組改為0x61
(a的編碼),我們發現buf1的輸出變成了auffer,而buf2的內容卻沒有發生變化,這也就印證了Buffer.from(buffer)是資料拷貝的觀點。
?注意:当Buffer的数据很大的时候,Buffer.from拷贝数据的性能是很差的,会造成CPU占用飙升,主线程卡死的情况,所以在使用这个函数的时候一定要清楚地知道Buffer.from(buffer)背后都做了什么。笔者就在实际项目开发中踩过这个坑,导致线上服务响应缓慢!
说完了buffer参数,我们再来说一下arrayBuffer参数,它的表现和buffer是有很大的区别的。ArrayBuffer是ECMAScript定义的一种数据类型,它简单来说就是一片你不可以直接(或者不方便)使用的内存,你必须通过一些诸如Uint16Array的TypedArray对象作为View来使用这片内存,例如一个Uint16Array对象的.buffer
属性就是一个ArrayBuffer对象。当Buffer.from函数接收一个ArrayBuffer作为参数时,Node会创建一个新的Buffer对象,不过这个Buffer对象指向的内容还是原来ArrayBuffer
的内容,没有任何的数据拷贝行为。我们来看个例子:
const arr = new Uint16Array(2) arr[0] = 5000 arr[1] = 4000 const buf = Buffer.from(arr.buffer) console.log(buf) // Prints: <Buffer 88 13 a0 0f> // 改变原来数组的数字 arr[1] = 6000 console.log(buf) // Prints: <Buffer 88 13 70 17>
从上面例子的输出我们可以知道,arr和buf对象会共用同一片内存空间,所以当我们改变原数组的数据时,buf的数据也会发生相应的变化。
其它Buffer操作
看完了创建Buffer的几种做法,我们接着来看一下Buffer其它的一些常用API或者属性
这个函数会返回当前buffer占用了多少字节
// 创建一个大小为1234字节的Buffer对象 const buf1 = Buffer.alloc(1234) console.log(buf1.length) // Prints: 1234 const buf2 = Buffer.from('Hello') console.log(buf2.length) // Prints: 5
这个字段表示Node会为我们预创建的Buffer池子有多大,它的默认值是8192,也就是8KB。Node在启动的时候,它会为我们预创建一个8KB大小的内存池,当用户用某些API(例如Buffer.alloc)创建Buffer实例的时候可能会用到这个预创建的内存池以提高效率,下面是一个具体的例子:
const buf1 = Buffer.from('Hello') console.log(buf1.length) // Prints: 5 // buf1的buffer属性会指向其底层的ArrayBuffer对象对应的内存 console.log(buf1.buffer.byteLength) // Prints: 8192 const buf2 = Buffer.from('World') console.log(buf2.length) // Prints: 5 // buf2的buffer属性会指向其底层的ArrayBuffer对象对应的内存 console.log(buf2.buffer.byteLength) // Prints: 8192
在上面的例子中,buf1
和buf2
对象由于长度都比较小所以会直接使用预创建的8KB内存池。其在内存的大概表示如图:这里值得一提的是只有当需要分配的内存区域小于4KB(8KB的一半)并且现有的Buffer池子还够用的时候,新建的Buffer才会直接使用当前的池子,否则Node会新建一个新的8KB的池子或者直接在内存里面分配一个区域(FastBuffer)。
这个函数可以按照一定的偏移量(offset)往一个Buffer实例里面写入一定长度(length)的数据。我们来看一下具体的例子:
const buf = Buffer.from('Hello') console.log(buf.toString()) // Prints: "Hello" // 从第3个位置开始写入'LLO'字符 buf.write('LLO', 2) console.log("HeLLO") // Prints: "HeLLO"
这里需要注意的是当我们需要写入的字符串的长度超过buffer所能容纳的最长字符长度(buf.length)时,超过长度的字符会被丢弃:
const buf = Buffer.from('Hello') buf.write('LLO!', 2) console.log(buf.toString()) // Print:s "HeLLO"
另外,当我们写入的字符长度超过buffer的最长长度,并且最后一个可以写入的字符不能全部填满时,最后一个字符整个不写入:
const buf = Buffer.from('Hello') buf.write('LL你', 2) console.log(buf.toString()) // Prints "HeLLo"
在上面的例子中,由于"你"是中文字符,需要占用三个字节,所以不能全部塞进buf里面,因此整个字符的三个字节都被丢弃了,buf对象的最后一个字节还是保持"o"不变。
这个函数可以用来拼接多个Buffer对象生成一个新的buffer。函数的第一个参数是待拼接的Buffer数组,第二个参数表示拼接完的buffer的长度是多少(totalLength)。下面是一个简单的例子:
const buf1 = Buffer.from('Hello') const buf2 = Buffer.from('World') const buf = Buffer.concat([buf1, buf2]) console.log(buf.toString()) // Prints "HelloWorld"
上面的例子中,因为我们没有指定最终生成Buffer对象的长度,所以Node会计算出一个默认值,那就是buf.totalLength = buf1.length + buf2.length
。而如果我们指定了totalLength的值的话,当这个值比buf1.lengh + buf2.length
小时,Node会截断最后生成的buffer;如果指定的值比buf1.length + buf2.length
大时,生成buf对象的长度还是totalLength,多出来的位数填充的内容是0。
这里还有一点值得指出的是,Buffer.concat最后拼接出来的Buffer对象是通过拷贝原来Buffer对象得出来,所以改变原来的Buffer对象的内容不会影响到生成的Buffer对象,不过这里我们还是需要考虑拷贝的性能问题就是了。
Buffer物件的垃圾回收
在文章剛開始的時候我就說過Node所有的Buffer物件分配的記憶體區域都是獨立於V8 堆空間的,屬於堆外記憶體。那麼是否這就意味著Buffer物件不受V8垃圾回收機制的影響需要我們手動管理記憶體了呢?其實不是的,我們每次使用Node的API建立一個新的Buffer物件的時候,每個Buffer物件都在JavaScript的空間對應著一個物件(Buffer記憶體的引用),這個物件是受V8垃圾回收控制的,而Node只需要在這個引用被垃圾回收的時候掛一些鉤子來釋放掉Buffer指向的堆外記憶體就可以了。 簡單來說Buffer分配的空間我們不需要操心,V8的垃圾回收機制會幫我們回收沒用的記憶體。
這篇文章我為大家介紹了Buffer的一些基礎知識,包括Buffer常用API和屬性,希望這些知識可以對你們的工作有所幫助。
更多node相關知識,請造訪:nodejs 教學!
以上是一文帶你深入了解Node中的Buffer類的詳細內容。更多資訊請關注PHP中文網其他相關文章!