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

Node.js模块的Buffer

时间:2023-04-03 13:58:28 Node.js

总之,在Node.js出现之前,JavaScript还是运行在浏览器端。处理Unicode编码的字符串数据很容易,但处理二进制和非Unicode编码的数据就无能为力了。但是需要在服务器端处理TCP和文件I/O。在Node.js中,提供了Buffer类来处理二进制数据,可以处理各种类型的数据。而在Node.js中,net、http、fs等一些重要的模块在数据传输和处理中都有Buffer,因为一些基础核心模块依赖于Buffer,所以在node启动的时候,Buffer就已经加载好了。我们可以直接在全局下使用Buffer。创建一个Buffer对象:newBuffer不安全?v6.0之前创建Buffer对象,直接使用newBuffer()构造函数创建对象实例,但是Buffer对内存的权限操作比较大,可以直接抓取一些敏感信息,所以v6.0之后,在官方文档推荐使用Buffer.from()接口创建Buffer对象。直接对比buffer.js的源码,看看两者的区别//Buffer构造函数Buffer(arg,encodingOrOffset,length)源码encodingOrOffset==='string'){thrownewError('如果指定了编码那么第一个参数必须是一个字符串');}返回Buffer.allocUnsafe(arg);}returnBuffer.from(arg,encodingOrOffset,length);}//Buffer.from函数源码Buffer.from=function(value,encodingOrOffset,length){if(typeofvalue==='number')thrownewTypeError('“值”参数不能是数字');if(isArrayBuffer(value)||isSharedArrayBuffer(value))returnfromArrayBuffer(value,encodingOrOffset,length);if(typeofvalue==='string')returnfromString(value,encodingOrOffset);从对象返回(值);};可以看到Buffer构造函数会判断第一个参数是否为数字类型,调用allocUnsafe接口或者直接调用from接口创建实例。两种创建方式唯一的区别在于,Buffer构造函数的第一个参数是第一个参数是数字类型,也就是说,如果我们使用构造函数方法创建,第一个参数不传递数字类型,和来自界面创建的逻辑一致,相对安全接下来我们看看为什么第一个传递的参数是数字,会存在安全隐患。如果第一个参数是数字,Buffer构造函数会为实例化的buffer分配一块内存空间,调用allocUnsafe接口对内存进行分类时,分配的内容空间没有初始化(数据没有重置),很可能携带缓存区前的数据。如果缓存中的内容是私钥、密码等敏感信息,则有可能被泄露,这里举例:varpassword='thisIsMyPassword';for(vari=0,i<100000;i++){varbuf=(newBuffer(200)).toString('ascii');if(buf.indexOf(token)!==-1){console.log('在i找到'+i+':'+buf);}}//密码内存申请的存储可能会在newBuffer中泄露出去,newBuffer()API的初始设计会让内存的分配非常快,因为不需要初始化和重置分配的内容空间每次。虽然具有一定的性能优势,但也存在一定的安全隐患。下面是具体的性能和耗时比较:console.time('new');for(vari=0;i<1000000;i++){newBuffer(2000);}console.timeEnd('new');console.time('alloc');for(vari=0;i<1000000;i++){Buffer.alloc(2000);}console.timeEnd('alloc');//运行结果,未初始化为fasterthaninitialized//new:1498ms//alloc:2439msv6.0之后的版本推荐使用Buffer.alloc()接口分配内存,使用Buffer.from()接口创建Buffer实例。同时,新版本还维护了Buffer.allocUnsafe()接口,但语义上面已经说的很清楚了。另外,在启用安全性方面,我们的业务--zero-fill-buffers默认启用了内存初始化。最后总结如下:使用newBuffer()构造创建Buffer对象实例的函数并非绝对不安全。alloc接口分配内存空间并初始化内存,不会泄漏旧缓存。必须有一个二进制的数据载体,ArrayBuffer对象、TypedArray对象、DataView对象都已经用JavaScript实现了,在ES6中被包含在ECMAScript规范中。事实上,这些数据结构在浏览器中也有使用,例如FileAPI、WebGL、Canvas、WebSockets等一些API的底层是二进制数据通信。查看node_buffer.cc的源码。Buffer在C++层面分配内存,最终以ArrayBuffer对象为载体。下面我们来区分一下ArrayBuffer、TypedArray和DataView的区别。ArrayBuffer对象:内存中的一段原始二进制数据,可以通过“查看”对其进行操作。TypedArray对象:用于生成内存视图。通过9个构造函数,可以生成9种数据格式视图。例如,Buffer中使用了Uint8Array(无符号8位整数)数组视图。DataView对象:暂时与本文无关,不再详细介绍。简单来说,Buffer模块使用v8::ArrayBuffer分配一块内存,通过v8::Uint8Array向TypedArray写入数据。说到Buffer内存分配,就不得不说到Buffer的8KB问题,对应buffer.js源码中的处理是Buffer.poolSize=8*1024;functionallocate(size){if(size<=0)返回新的FastBuffer();if(size>>1)if(size>poolSize-poolOffset)createPool();varb=allocPool.slice(poolOffset,poolOffset+size);poolOffset+=大小;对齐池();returnb}else{returncreateUnsafeBuffer(大小);边界,如果写入的数据大于8KB的一半,则直接分配内存,如果小于4KB,则从当前分配池判断是否有足够的空间用于当前存储的数据,如果没有,则重新申请8KB的内存空间,把数据存放在新申请的空间里。如果有足够的写入,则直接将数据写入内存空间。下图展示了它的内存分配策略。如上图,如果当前存储了2KB的数据,后面要存储5KB的数据时,分配池判断需要的内存空间大于4KB,就会重新申请内存空间存储5KB数据的和分配池的当前偏移量指针也指向新申请的内存空间,此时,剩余的6KB(8KB-2KB)内存空间将被搁置。至于为什么使用8KB作为存储单元分配,这里不做深入研究。另外,Buffer的单个内存分配也是有限制的,这个限制根据不同的操作系统不同,这个限制可以在node_buffer.h中看到staticconstunsignedintkMaxLength=sizeof(int32_t)==sizeof(intptr_t)?0x3fffffff:0x7fffffff;对于32位操作系统,一次可以分配的最大内存是1G,对于64位或更高版本的操作系统,是2GBuffer和StringBuffer和String都可以存储字符串类型的数据,只是String不同来自缓冲区。对于内存分配,String直接使用v8堆存储,没有在C++堆外分配内存,Google也对String进行了优化。在实际的拼接速度对比中,String要快于Buffer。但是Buffer的出现是为了处理二进制等非Unicode编码的数据,所以需要使用Buffer来处理非utf8数据。编码支持ascii-仅支持7位ASCII数据。utf8-Unicode字符的多字节编码utf16le-2或4字节,Unicode字符的小端编码base64-Base64字符串编码binary-二进制编码。hex-将每个字节编码为两个十六进制字符。