热点聚焦:一文带你深入了解Node中的Buffer类

时间:2022-12-12 20:00:22       来源:转载
本篇文章带大家深入了解下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。

字符编码

既然所有数据底层都是二进制,网络传输的数据也是二进制的话,为什么我们现在阅读的文章是中文而不是一堆01呢?这里就要介绍一下字符编码的概念了。所谓的字符编码简单来说就是一个映射关系表,它表示的是字符(中文字符、英文字符或者其它字符)是如何和二进制数字(包含若干个字节)对应起来的。举个例子,如果使用我们熟悉的ascii来编码,a这个英文字符的二进制表示是0b01100001(0b是二进制数字的前缀)。因此当我们的电脑从某个以ascii编码的文件中读取到0b01100001这串二进制数据时,就会在屏幕中显示a这个字符,同样a这个字符保存到计算机中或者在网络上传输都是0b01100001这个二进制数据。除了ascii码,常见的字符编码还有utf-8utf-16等。

Buffer


掌握了基本的二进制知识字符编码的概念后,我们终于可以正式学习Buffer了。我们先来看一下Buffer的官方定义:

简单来说所谓的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: 
登录后复制

上面的代码中我创建了一个大小为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.allocUnsafe分配出来的buffer内容的。也正是由于不对分配过来的内存进行初始化所以这个函数分配Buffer的速度会比Buffer.alloc更快,我们在实际开发中应该根据自己实际的需要进行取舍。

Buffer.from

这个函数是我们最常用的创建Buffer的函数,它有很多不同的重载,也就是说传入不同的参数会有不同的表现行为。我们来看几个常见的重载:

Buffer.from(string[, encoding])

当我们传入的第一个参数是字符串类型时,Buffer.from会根据字符串的编码(encoding参数,默认是utf8)生成该字符串对应的二进制表示。看个例子:

const buff = Buffer.from("你好世界")console.log(buff)// Prints: 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: console.log(buf2)// Prints: buf1[0] = 0x61console.log(buf1.toString())// Prints: aufferconsole.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] = 5000arr[1] = 4000const buf = Buffer.from(arr.buffer)console.log(buf)// Prints: // 改变原来数组的数字arr[1] = 6000console.log(buf)// Prints: 
登录后复制

从上面例子的输出我们可以知道,arrbuf对象会共用同一片内存空间,所以当我们改变原数组的数据时,buf的数据也会发生相应的变化。

其它Buffer操作

看完了创建Buffer的几种做法,我们接着来看一下Buffer其它的一些常用API或者属性

buf.length

这个函数会返回当前buffer占用了多少字节

// 创建一个大小为1234字节的Buffer对象const buf1 = Buffer.alloc(1234)console.log(buf1.length)// Prints: 1234const 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: 8192const buf2 = Buffer.from("World")console.log(buf2.length)// Prints: 5// buf2的buffer属性会指向其底层的ArrayBuffer对象对应的内存console.log(buf2.buffer.byteLength)// Prints: 8192
登录后复制

在上面的例子中,buf1buf2对象由于长度都比较小所以会直接使用预创建的8KB内存池。其在内存的大概表示如图:这里值得一提的是只有当需要分配的内存区域小于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相关知识,请访问:nodejs 教程!

以上就是一文带你深入了解Node中的Buffer类的详细内容,更多请关注php中文网其它相关文章!

关键词: 一个新的 垃圾回收 二进制数