您好,登錄后才能下訂單哦!
這篇文章主要介紹了Node中的Buffer類怎么使用的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Node中的Buffer類怎么使用文章都會有所收獲,下面我們一起來看看吧。
在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個字節來表示的話,底層存儲結構是:我們可以看到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的官方定義:
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 usingconsole.log()
to print theBuffer
instance, 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的垃圾回收機制會幫我們回收掉沒用的內存。
關于“Node中的Buffer類怎么使用”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Node中的Buffer類怎么使用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。