Dieser Artikel vermittelt Ihnen ein tiefgreifendes Verständnis der Buffer-Klasse in Node. Ich hoffe, er wird Ihnen hilfreich sein!
Bevor das TypedArray herauskam, konnte die JavaScript-Sprache rohe Binärdaten (Rohbinärdaten) nicht gut verarbeiten, da JavaScript zu Beginn hauptsächlich in Browsern als Skriptsprache verwendet wurde Daher gibt es nur sehr wenige Szenarien, in denen native Binärdaten verarbeitet werden müssen. Nachdem Node herauskam, definierte Node zusätzlich zu JavaScript einen neuen Datentyp Buffer, da serverseitige Anwendungen eine große Anzahl von Binärströmen wie Dateilesen und -schreiben, TCP-Verbindungen usw. verarbeiten müssen (V8) . Da Buffer in Node-Anwendungen weit verbreitet ist, können Sie nur dann bessere Node-Anwendungen schreiben, wenn Sie seine Verwendung wirklich beherrschen. [Empfohlene verwandte Tutorials: nodejs-Video-Tutorial, Programmierunterricht]
Bevor wir die spezifische Verwendung von Buffer offiziell vorstellen, wollen wir kurz das Wissen über Binärdateien überprüfen.
Als Programmierer sollten wir alle mit Binärformaten vertraut sein, da alle zugrunde liegenden Daten des Computers im Binärformat gespeichert sind. Mit anderen Worten: Die Dateien auf Ihrem Computer, egal ob es sich um reine Texte, Bilder oder Videos handelt, bestehen aus den beiden Zahlen 01 auf der Festplatte des Computers. In der Informatik nennen wir eine einzelne Zahl 0 oder 1 ein Bit(Bit), und 8 Bits können ein Byte(Byte) bilden. Wenn die Dezimalzahl 16 durch 1 Byte dargestellt wird, ist die zugrunde liegende Speicherstruktur: Wir können sehen, dass es bei der binären Darstellung von 16 6 weitere Ziffern im Vergleich zur Dezimaldarstellung gibt, wenn die Zahl größer ist Da Binärziffern größer sind, ist das Lesen und Schreiben für uns sehr umständlich. Aus diesem Grund verwenden Programmierer im Allgemeinen gerne Hexadezimal (Hexadezimal) zur Darstellung von Daten, anstatt direkt Binärdaten zu verwenden. Wenn wir beispielsweise CSS schreiben, verwenden wir für den Wert von Farbe Hexadezimalzahlen (z. B. #FFFFFF) anstelle eines Bündels aus 0ern und 1ern.
ZeichenkodierungDa die unterste Ebene aller Daten binär ist und die über das Netzwerk übertragenen Daten ebenfalls binär sind, warum sind die Artikel, die wir jetzt lesen,
Chinesischund nicht eine Menge 0? und 1? Hier stellen wir das Konzept der Zeichenkodierung vor. Die sogenannte Zeichenkodierung ist einfach eine Mapping-Beziehungstabelle, die darstellt, wie Zeichen (chinesische Zeichen, englische Zeichen oder andere Zeichen) Binärzahlen (einschließlich mehrerer Bytes) entsprechen. Wenn wir zum Beispiel das bekannte ascii zum Codieren verwenden, lautet die binäre Darstellung des englischen Zeichens a 0b01100001 (0b ist das Präfix einer Binärzahl). Wenn unser Computer daher die Binärdatenfolge 0b01100001 aus einer in ASCII codierten Datei liest, wird das Zeichen a auf dem Bildschirm angezeigt und das Zeichen a wird auch auf dem Computer gespeichert Die im Netzwerk übertragenen Binärdaten sind 0b01100001. Zu den gängigen Zeichenkodierungen gehören neben ascii-Codes auch utf-8 und utf-16 usw. Buffer
Zeichenkodierung beherrschen, können wir Buffer endlich formal lernen. Werfen wir einen Blick auf die offizielle Definition von Buffer:
In der
简单来说所谓的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对象,不过这里我们还是需要考虑拷贝的性能问题就是了。
Garbage Collection von Pufferobjekten
Am Anfang des Artikels habe ich gesagt, dass die von allen Pufferobjekten im Knoten zugewiesenen Speicherbereiche unabhängig vom V8-Heapspeicher sind und zum Off-Heap-Speicher gehören. Bedeutet das also, dass das Buffer-Objekt nicht vom V8-Garbage-Collection-Mechanismus betroffen ist und wir den Speicher manuell verwalten müssen? Eigentlich nein, jedes Mal, wenn wir die API von Node verwenden, um ein neues Pufferobjekt zu erstellen, entspricht jedes Pufferobjekt einem Objekt (einem Verweis auf den Pufferspeicher) im JavaScript-Bereich. Dieses Objekt wird von der V8-Garbage Collection gesteuert Hängen Sie einige Hooks ein, um den Off-Heap-Speicher freizugeben, auf den der Puffer zeigt, wenn diese Referenz Garbage Collection ist. Einfach gesagt, wir müssen uns keine Sorgen darüber machen, dass der von Buffer V8 zugewiesene Speicherplatz uns dabei hilft, nutzlosen Speicher zurückzugewinnen. Zusammenfassung
!
Das obige ist der detaillierte Inhalt vonDieser Artikel vermittelt Ihnen ein detailliertes Verständnis der Buffer-Klasse in Node. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!