一文帶你深入瞭解Node中的Buffer類

2022-12-12 22:02:32
本篇文章帶大家深入瞭解下中 Buffer(緩衝區)類,希望對大家有所幫助!

node.js極速入門課程:進入學習

TypedArray出來之前,JavaScript這門語言是不能很好地處理原始二進位制資料(raw binary data)的,這是因為一開始的時候JavaScript主要還是應用在瀏覽器中作為指令碼語言使用,所以需要處理原生二進位制資料的場景是少之又少。而Node出來後,由於伺服器端的應用需要處理大量的二進位制流例如檔案讀寫TCP連線等,所以Node在JavaScript(V8)之外,定義了一種新的資料型別Buffer。由於Buffer在Node應用中使用十分廣泛,所以只有真正掌握了它的用法,你才能寫出更好的Node應用。【相關教學推薦:、】

二進位制基礎


在正式介紹Buffer的具體用法之前,我們先來簡單回顧一下有關二進位制的知識。

身為程式設計師,我們應該都不會對二進位制感到陌生,因為計算機所有的資料底層都是以二進位制(binary)的格式儲存的。換句話來說你電腦裡面的檔案,不管是純文字還是圖片還是視訊,在計算機的硬碟裡面都是由01這兩個數位組成的。在電腦科學中我們把0或者1單個數位叫做一個位元(bit),8個位元可以組成一個位元組(byte)。十進位制(decimal)數位16如果用1個位元組來表示的話,底層儲存結構是:截圖2022-10-15 下午2.23.13.png我們可以看到16用二進位制表示的話相比於十進位制的表示一下子多了6位數位,如果數位再大點的話二進位制的位數會更多,這樣我們無論是閱讀還是編寫起來都很不方便。因為這個原因,程式設計師一般喜歡用十六進位制(hexadecimal)來表示資料而不是直接使用二進位制,例如我們在寫CSS的時候color的值用的就是16進位制(例如#FFFFFF)而不是一堆0和1。

字元編碼

既然所有資料底層都是二進位制,網路傳輸的資料也是二進位制的話,為什麼我們現在閱讀的文章是中文而不是一堆01呢?這裡就要介紹一下字元編碼的概念了。所謂的字元編碼簡單來說就是一個對映關係表,它表示的是字元(中文字元、英文字元或者其它字元)是如何和二進位制數位(包含若干個位元組)對應起來的。舉個例子,如果使用我們熟悉的ascii來編碼,a這個英文字元的二進位制表示是0b01100001(0b是二進位制數位的字首)。因此當我們的電腦從某個以ascii編碼的檔案中讀取到0b01100001這串二進位制資料時,就會在螢幕中顯示a這個字元,同樣a這個字元儲存到計算機中或者在網路上傳輸都是0b01100001這個二進位制資料。除了ascii碼,常見的字元編碼還有utf-8utf-16等。

Buffer


掌握了基本的二進位制知識字元編碼的概念後,我們終於可以正式學習Buffer了。我們先來看一下Buffer的官方定義:

The Buffer 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 buffer each represent a byte and so are limited to values from 0 to 255 inclusive. When using console.log() to print the Buffer instance, you'll get a chain of values in hexadecimal values.

簡單來說所謂的Buffer就是Node在V8堆記憶體之外分配的一塊固定大小的記憶體空間。當Buffer被用console.log列印出來時,會以位元組為單位,列印出一串以十六進位制表示的值。

建立Buffer

瞭解完Buffer的基本概念後,我們再來建立一個Buffer物件。建立Buffer的方式有很多種,常見的有Buffer.allocBuffer.allocUnsafeBuffer.from

Buffer.alloc(size[, fill[, encoding]])

這是最常見的建立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預設的行為,我們可以設定後面兩個引數fillencoding來指定初始化的時候填入另外的內容。

這裡值得一提的是我在上面的程式碼中使用的是Node全域性的Buffer物件,而沒有從node:buffer包中顯式匯入,這完全是因為編寫方便,在實際開發中應該採用後者的寫法:

import { Buffer } from 'node:buffer'
登入後複製

Buffer.allocUnsafe(size)

Buffer.allocUnsafeBuffer.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.from

這個函數是我們最常用的建立Buffer的函數,它有很多不同的過載,也就是說傳入不同的引數會有不同的表現行為。我們來看幾個常見的過載:

Buffer.from(string[, encoding])

當我們傳入的第一個引數是字串型別時,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)

當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.from(arrayBuffer[, byteOffset[, length]])

說完了buffer引數,我們再來說一下arrayBuffer引數,它的表現和buffer是有很大的區別的。ArrayBuffer是ECMAScript定義的一種資料型別,它簡單來說就是一片你不可以直接(或者不方便)使用的記憶體,你必須通過一些諸如Uint16ArrayTypedArray物件作為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>
登入後複製

從上面例子的輸出我們可以知道,arrbuf物件會共用同一片記憶體空間,所以當我們改變原陣列的資料時,buf的資料也會發生相應的變化。

其它Buffer操作

看完了建立Buffer的幾種做法,我們接著來看一下Buffer其它的一些常用API或者屬性

buf.length

這個函數會返回當前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
登入後複製

Buffer.poolSize

這個欄位表示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
登入後複製

在上面的例子中,buf1buf2物件由於長度都比較小所以會直接使用預建立的8KB記憶體池。其在記憶體的大概表示如圖:截圖2022-12-11 下午1.51.54.png這裡值得一提的是隻有當需要分配的記憶體區域小於4KB(8KB的一半)並且現有的Buffer池子還夠用的時候,新建的Buffer才會直接使用當前的池子,否則Node會新建一個新的8KB的池子或者直接在記憶體裡面分配一個區域(FastBuffer)。

buf.write(string[, offset,[, length]][, encoding])

這個函數可以按照一定的偏移量(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.concat(list[, totalLength])

這個函數可以用來拼接多個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相關知識,請存取:!

以上就是一文帶你深入瞭解Node中的Buffer類的詳細內容,更多請關注TW511.COM其它相關文章!