当前位置: 首页 > 后端技术 > Node.js

Node.js中的缓冲区究竟是什么?

时间:2023-04-03 20:01:12 Node.js

大多数人都有自己不知道的能力和机会,也有可能做自己做梦都想不到的事情。——从前端转到Node.js的DaleCarnegie童鞋对这部分会比较陌生,因为前端一些简单的字符串操作已经满足了基本的业务需求,有时候你可能会觉得Buffer和Stream是很有用。神秘。回到服务端,如果你不想只是一个普通的Node.js开发工程师,你应该多了解Buffer来揭开这层谜团,同时,也会提高你对Buffer的理解Node.js更上一层楼。作者简介:MayJun,NodejsDeveloper,90后,热爱技术,热爱分享,公众号《Nodejs技术栈》,Github开源项目https://www.nodejs.redBuffer初识JavaScript语言之前引入TypedArray没有读取或操作二进制数据流的机制。Buffer类是作为Node.jsAPI的一部分引入的,用于与TCP流、文件系统操作和其他上下文中的八位字节流进行交互。这是Node.js官网的描述,比较晦涩难懂。综上所述,Node.js可用于处理或与二进制流数据交互。缓冲区用于读取或操作二进制数据流。当用作Node.jsAPI的一部分时,它不需要require。用于操作需要大量二进制数据的网络协议、数据库、图像、文件I/O场景。Buffer的大小在创建时就已经确定,无法调整。这个Buffer的内存分配是C++级别提供的,而不是V8,后面会解释。不知道大家觉得这里简单吗?但是上面提到的二进制、流(Stream)、缓冲区(Buffer)这些关键字有哪些呢?让我们尝试做一些简单的介绍。什么是二进制数据?说到二进制,我们的大脑可能会想到010101这样的代码指令,如下图所示:如上图所示,二进制数据由0和1两个数字来表示。为了存储或显示一些数据,计算机需要先将这些数据转换为二进制表示。比如我要存储数字66,计算机会先把数字66转换成二进制01000010。我第一次接触这个是在大学期间的C语言课上。转换公式如下:128643216842101000010。比如我们知道数字只是其中一种数据类型,其他还有字符串、图片、文件等。比如我们对一个英文的M进行操作,在JavaScript中通过'M'.charCodeAt()(通过上述步骤)得到对应的ASCII码后,会转为二进制表示。什么是流?Stream,英文Stream是输入输出设备的抽象,其中设备可以是文件、网络、内存等,Streams是有方向的。当程序从数据源读取数据时,将打开一个输入流。这里的数据源可以是文件也可以是网络。比如我们从一个.txt文件中读取数据。相反,当我们的程序需要向指定的数据源(文件、网络等)写入数据时,就会打开一个输出流。当有一些大文件操作的时候,我们就需要Stream像流水线一样把数据一点一点的流出来。比如我们现在有一大盆水需要倒到菜地里。如果我们一下子把水壶里的水全部倒进菜地里,首先需要多大的力气(这里的力气就像电脑里的硬件性能)才能动起来。如果我们用一根水管把水一点一点地往我们的菜地里灌,这时候就不用那么费力了。通过上面的讲解,是不是对什么是Stream有了进一步的了解呢?那么Stream和Buffer是什么关系呢?请看下面的介绍。Stream本身也有很多知识点。欢迎关注公众号《Nodejs技术栈》,后面会单独介绍。什么是缓冲区?通过上面对Stream的解释,我们看到了数据是从一端流向另一端,那么它们是如何流动的呢?通常,移动数据是为了处理或读取它并根据它做出决策。随着时间的推移,每个进程将具有最小或最大数据量。如果数据到达的速度快于进程可以消耗它的速度,则较早到达的少数数据将位于等待区等待处理。相反,如果数据到达的速度比进程消耗的速度慢,那么早到达的数据将不得不等待一定数量的数据到达才能被处理。这里的等待区指的是缓冲区(Buffer),它是计算机中的一个小物理单元,通常位于计算机的RAM中。这些概念可能比较难理解,别着急,我们举个例子更进一步。举一个在公交车站乘公交车的例子。举一个在公交车站乘公交车的例子。通常,巴士每隔几十分钟一班。即使这个时间之前乘客已经满了,公交车也不会提前发车。你需要先在车站等候。假设到达的乘客太多,其中一些人将需要在公共汽车站等候下一班公共汽车的到来。上面例子中候车区的公交车站对应我们Node.js中的buffer。此外,乘客到达的速度也是我们无法控制的。我们能控制的是公交车什么时候出发,对应于在我们的程序中,我们无法控制数据流的到达时间,我们能做的是决定什么时候发送数据。Buffer的基本使用了解了Buffer的一些概念之后,我们来看一下Buffer的一些基本使用。这里我们不会列出所有的API使用,只列出一些常用的。更多详情请参考Node.js中文网。创建缓冲区在6.0.0之前的Node.js版本中,Buffer实例是使用Buffer构造函数创建的,它根据提供的参数以不同方式分配newBuffer()返回的Buffer。Buffer.from()constb1=Buffer.from('10');constb2=Buffer.from('10','utf8');constb3=Buffer.from([10]);constb4=Buffer.from(b3);console.log(b1,b2,b3,b4);//Buffer.alloc返回一个初始化的Buffer,保证新创建的Buffer永远不会包含旧数据。constbAlloc1=Buffer.alloc(10);//创建一个大小为10字节的缓冲区console.log(bAlloc1);//Buffer.allocUnsafe创建一个大小为字节的新的未初始化缓冲区。由于Buffer未初始化,分配的内存段可能包含敏感的旧数据。当Buffer内容可读时,其旧数据可能会泄露,不安全,需谨慎使用。constbAllocUnsafe1=Buffer.allocUnsafe(10);console.log(bAllocUnsafe1);//Buffer字符编码通过使用字符编码,可以实现Buffer实例和JavaScript字符串的相互转换,目前支持的字符编码如下:'ascii'-only适用于7位ASCII数据。这种编码速度很快,如果设置的话会去掉高位。'utf8'-Unicode字符的多字节编码。许多网页和其他文档格式都使用UTF-8。'utf16le'-2或4个字节,little-endian编码的Unicode字符。支持代理对(U+10000到U+10FFFF)。'ucs2'-'utf16le'的别名。'base64'-Base64编码。从字符串创建Buffer时,此编码还可以正确接受RFC4648第5节中指定的“URL和文件名安全字母”。'latin1'-一种将Buffer编码为单字节编码字符串的方法(由IANA在RFC1345,第63页中定义,作为Latin-1和C0/C1控制代码的补充块)。'binary'-'latin1'的别名。'hex'-将每个字节编码为两个十六进制字符。constbuf=Buffer.from('helloworld','ascii');console.log(buf.toString('hex'));//68656c6c6f20776f726c64string和Buffer类型相互转换StringtoBuffer这个大家不陌生吧,通过上面讲解的Buffer.form(),如果不传编码,默认会转换为UTF-8格式存储。constbuf=Buffer.from('Node.js技术栈','UTF-8');console.log(buf);//<缓冲区4e6f64652e6a7320e68a80e69cafe6a088>console.log(buf.length);//17Buffer转字符串Buffer转字符串也很简单,使用toString([encoding],[start],[end])方法,默认编码还是UTF-8,如果不传start,end可以实现全量转换,通过start,end可以实现部分转换(这里要注意}constbuf=Buffer.from('Node.js技术栈','UTF-8');console.log(buf);//<缓冲区4e6f64652e6a7320e68a80e69cafe6a088>console.log(buf.length);//17console.log(buf.toString('UTF-8',0,9));//Node.js?runandcheck,可以看到上面的输出是Node.js?出现乱码,为什么?转换时为什么会出现乱码?首先,上面例子中使用的默认编码方式是UTF-8.问题是UTF-8下一个汉字占3个字节,buf中对应的字节是8a80e6而我们的设置范围是0到9,所以只输出8a,这时候字符会是将出现截断和乱码字符。下面我们改变一下例子的拦截范围:constbuf=Buffer.from('Node.jstechnologystack','UTF-8');控制台日志(buf);//<缓冲区4e6f64652e6a7320e68a80e69cafe6a088>console.log(buf.length);//17console.log(buf.toString('UTF-8',0,11));//Node.js技术可以看到Buffer内存机制已经正常输出。Nodejs中的内存管理和V8垃圾回收机制这一节主要讲解了V8在Node.js的垃圾回收中主要是如何进行管理的,但是并没有提到Buffer类型的数据是如何被回收的,让我们了解一下Buffer的内存回收机制。由于Buffer需要处理大量的二进制数据,如果一点点申请到系统,会导致系统内存调用频繁,所以Buffer占用的内存不再由V8分配,而是在Node.js中分配。js在C++层面完成应用,在JavaScript中进行内存分配。因此,我们称这部分内存为堆外内存。注:下面使用的buffer.js源码为Node.jsv10.x版本,地址:https://github.com/nodejs/node/blob/v10.x/lib/buffer.jsBuffer内存分配原理Node.jsslab机制用于预申请和后分配,是一种动态管理机制。使用Buffer.alloc(size)传入一个指定的size,就会申请一块固定大小的内存区域。slab有以下三种状态:full:完全分配状态partial:部分分配状态empty:未分配状态8KBlimitNode.js以8KB为界限来区分小对象和大对象。在buffer.js中可以看到如下代码Buffer.poolSize=8*1024;//102行,Buffer中Node.js版本为v10.x部分提到,Buffer的大小在创建时就已经确定,无法调整。这里应该理解。Buffer对象分配下面的代码示例,加载时直接调用createPool()相当于直接初始化一个8KB的内存空间,在第一次分配内存时也会变得更加高效。另外在初始化的同时初始化了一个新的变量poolOffset=0。该变量将记录已使用了多少字节。Buffer.poolSize=8*1024;varpoolSize,poolOffset,allocPool;...//中间代码省略functioncreatePool(){poolSize=Buffer.poolSize;allocPool=createUnsafeArrayBuffer(poolSize);poolOffset=0;}createPool();//129行此时新建的slab如下:现在让我们尝试分配一个大小为2048的Buffer对象,代码如下:Buffer.alloc(2*1024)现在让我们看一下目前的slab内存是什么样的?如下:那么这个分配过程是怎样的呢?再来看buffer.js的另一个核心方法allocate(size)//https://github.com/nodejs/node/blob/v10.x/lib/buffer.js#L318functionallocate(size){if(size<=0){返回新的FastBuffer();}//当分配的空间小于Buffer.poolSize向右移动时,这里的结果是4KBif(size<(Buffer.poolSize>>>1)){if(size>(poolSize-poolOffset))createPool();varb=newFastBuffer(allocPool,poolOffset,size);poolOffset+=大小;//累积使用的空间alignPool();//8字节内存对齐处理returnb;}else{//在C++级别应用returncreateUnsafeBuffer(size);}}看完上面的代码,就可以清楚的看出什么时候分配小的Buffer对象,什么时候分配大的Buffer对象。Buffermemoryallocation的总结,真的很难看懂。看了几本Node.js相关的书,朴玲老师的《Node.jsIntroductioninSimple》Buffer部分写的还是比较详细的。我建议你阅读它。第一次加载时会初始化一个8KB的内存空间。buffer.js源码显示,根据申请的内存大小,分为小Buffer对象和大Buffer对象。SmallBuffer会不断判断slab空间是否足够。要同时使用剩余空间和更新slab分配状态,offset会增加。如果空间和slab空间不足,会创建一个新的slab空间分配一个大的Buffer。在这种情况下,将直接使用createUnsafeBuffer(size)函数。小的Buffer对象仍然是大的Buffer对象。内存分配在C++级别完成,内存管理在JavaScript级别完成。最终,它仍然可以被V8的垃圾回收标志回收。Buffer应用场景以下是Buffer在实际业务中的一些应用场景,欢迎大家在评论区补充!I/O操作I/O可以是文件或网络I/O。下面是通过流读取input.txt的信息,然后写入到output.txt文件中。不明白Stream和Buffer的关系。回头看看什么是Stream?什么是缓冲区?constfs=require('fs');constinputStream=fs.createReadStream('input.txt');//创建可读流constoutputStream=fs.createWriteStream('output.txt');//创建一个可写流inputStream.pipe(outputStream);//管道读写在Stream中,我们不需要手动创建自己的缓冲区,在Node.js流将自动创建。zlib.jszlib.js是Node.js的核心库之一,利用缓冲区(Buffer)的功能对二进制数据流进行操作,并提供压缩或解压功能。参考源代码。zlib.js源码加解密在一些加解密算法中会用到Buffer。比如crypto.createCipheriv的第二个参数key是String或者Buffer类型。如果是Buffer类型,我们就使用本文讲解的内容。内容,下面是一个简单的加密例子,重点是使用Buffer.alloc()初始化一个实例(这个上面有介绍),然后使用fill方法进行填充,这里重点说说这个方法的使用。buf.fill(value[,offset[,end]][,encoding])value:第一个参数为要填充的内容offset:偏移量,填充起始位置end:填充结束的偏移量bufencoding:代码下面设置的是Cipher的对称加密Democonstcrypto=require('crypto');const[key,iv,algorithm,encoding,cipherEncoding]=['a123456789','','aes-128-ecb','utf8','base64'];consthandleKey=key=>{constbytes=Buffer.alloc(16);//初始化一个Buffer实例,每一项填充00console.log(bytes);//<缓冲区00000000000000000000000000000000>bytes.fill(key,0,10)//填充console.log(bytes);//returnbytes;}letcipher=crypto.createCipheriv(algorithm,handleKey(key),iv);letcrypted=cipher.update('Node.js技术栈',encoding,cipherEncoding);crypted+=cipher.final(cipherEncoding);console.log(crypted)//jE0ODwuKN6iaKFKqd3RF4xFZkOpasy8WfIDl8tRC5t0=BufferVSCacheBuffer和Cache有什么区别?缓冲区(Buffer)缓冲区(Buffer)用于处理二进制流数据,并对数据进行缓冲。这是暂时的。对于流式数据,缓冲区将用于临时存储数据等。达到一定大小后,存储在硬盘中。视频播放器就是一个典型的例子。有时你会看到一个缓冲区图标,这意味着此时这组缓冲区还没有满。当数据到达缓冲区满区后,缓冲区图标消失,可以看到一些图像数据。缓存(Cache)可以看作是一个中间层,可以永久缓存热数据,使访问更快。比如我们从硬盘或者其他第三方通过Memory、Redis等方式传输数据,接口缓存请求的目的是将数据存储在内存的缓存区,这样访问同一个资源会更快,这也是性能优化的一个重要点。来自知乎的讨论,点击更多查看BufferVSString通过压力测试看String和Buffer的性能如何?consthttp=require('http');lets='';for(leti=0;i<1024*10;i++){s+='a'}conststr=s;constbufStr=Buffer.from(s);constserver=http.createServer((req,res)=>{console.log(req.url);if(req.url==='/buffer'){res.end(bufStr);}else如果(req.url==='/string'){res.end(str);}});server.listen(3000);上面的例子我是放在虚拟机里测试的,你也可以在本地电脑上测试,使用AB测试工具。测试字符串查看以下重要参数指标,然后通过缓冲区传输进行比较完成请求:21815Requestspersecond:363.58[#/sec](mean)Transferrate:3662.39[Kbytes/sec]received$ab-c200-t60http://192.168.6.131:3000/string测试缓冲区,可以看到通过缓冲区传输的请求总数为50000,QPS增加了一倍多,每秒传输的字节数为9138.82KB。从这些数据可以证明,提前将数据转换成Buffer的方法,可以将性能提高近一倍。完成请求:50000每秒请求数:907.24[#/sec](平均值)传输速率:9138.82[Kbytes/sec]收到$ab-c200-t60http://192.168.6.131:3000/bufferHTTP传输中的传输它是二进制数据。上例中的/string接口直接返回字符串。此时HTTP会在传输前将字符串转换为Buffer类型,传输二进制数据,返回给客户端。但是直接返回Buffer类型,减少了每次转换操作的次数,也提高了性能。在某些Web应用中,可以将静态数据提前转换为Buffer进行传输,可以有效减少CPU重用(重复的string-to-Buffer操作)。参考http://nodejs.cn/api/buffer.htmlNode.jsBuffer简介想更深入的了解Node.js中的Buffer吗?看看这个。ArrayBuffers和SharedArrayBuffersbuffer.jsv10.x的卡通介绍