1

简言

在没有出现Node.js之前,JavaScript还是运行在浏览器端,对于处理Unicode编码的字符串数据很容易,但是对于处理二进制以及非Unicode编码的数据无能为力,但是对于Server端操作TCP以及文件I/O的处理是必须的。在Node.js里面提供了Buffer类处理二进制的数据,可以处理各种类型的数据。并且在Node.js里面一些重要模块net、http、fs中的数据传输以及处理都有Buffer的身影,因为一些基础的核心模块都要依赖Buffer,所以在node启动的时候,就已经加载了Buffer,我们可以在全局下面直接使用Buffer。


创建Buffer对象:new Buffer不安全?

在v6.0之前创建Buffer对象直接使用new Buffer()构造函数来创建对象实例,但是Buffer对内存的权限操作相比很大,可以直接捕获一些敏感信息,所以在v6.0以后,官方文档里面建议使用Buffer.from()接口去创建Buffer对象。直接对比buffer.js的源码,看看两者有什么区别

// Buffer构造函数的源码
function Buffer(arg, encodingOrOffset, length) 
{
    if (typeof arg === 'number') 
    {
        if (typeof encodingOrOffset === 'string') 
        {
          throw new Error(
            'If encoding is specified then the first argument must be a string'
          );
        }
        return Buffer.allocUnsafe(arg);
    }
  return Buffer.from(arg, encodingOrOffset, length);
}
// Buffer.from函数的源码
Buffer.from = function(value, encodingOrOffset, length) 
{
  if (typeof value === 'number')
    throw new TypeError('"value" argument must not be a number');

  if (isArrayBuffer(value) || isSharedArrayBuffer(value))
    return fromArrayBuffer(value, encodingOrOffset, length);

  if (typeof value === 'string')
    return fromString(value, encodingOrOffset);

  return fromObject(value);
};

由源码里面可以看到Buffer构造函数里面会判断第一个参数是否为数字类型而调用allocUnsafe接口或者是直接调用from接口去创建实例,而这两种创建对象的唯一区别就在于Buffer构造函数方式的第一个参数是数字类型,那么就是说,如果我们使用构造函数方式创建时候第一个参数不传数字类型就和from接口创建的逻辑是一致的,相对会更加安全。接下来看为啥如果第一个参数传递是数字的话会可能存在安全风险,如果第一个参数是数字,Buffer构造函数会去分配一个内存空间给到实例化的buffer使用,而调用allocUnsafe接口去分类内存的时候,分配出来的内容空间是没有被初始化(数据没被重置),很有可能会携带该缓存区之前的数据,如果缓存里面的内容是一些私钥、密码等敏感信息的话就可有可能被泄漏出去,下面举个例子:

var password = 'thisIsMyPassword';
for( var i = 0, i < 100000; i++ ) {
  var buf = (new Buffer(200)).toString('ascii');
  if (buf.indexOf(token) !== -1) {
     console.log('Found at i ' + i + ': ' + buf);
  }
}
// password内存申请的存储可能在new Buffer里面泄漏出去

而最初new Buffer()API这样设计的会使得内存的分配非常快,因为不用每次都不用去初始化重置分配到的内容空间,虽然有一定的性能优势,但是也有一定的安全风险,下面是具体的性能耗时对比:

console.time('new');
for( var i = 0;i< 1000000;i++) {
    new Buffer(2000);
}
console.timeEnd('new');

console.time('alloc');
for( var i = 0;i< 1000000;i++) {
    Buffer.alloc(2000);
}
console.timeEnd('alloc');

// 运行结果,不初始化比初始化更快
// new: 1498ms
// alloc: 2439ms

v6.0之后的版本都建议使用Buffer.alloc()接口去分配内存,以及使用Buffer.from()接口去创建Buffer实例,与此同时,新版也维持Buffer.allocUnsafe()接口,但是语义上面已经说的明确,此外,在开启安全方面,我们业务–zero-fill-buffers来默认启用内存初始化,最后以下是总结:

  • 使用new Buffer()构造函数创建Buffer对象实例并非绝对的不安全

  • alloc接口分配内存空间会初始化内存,不会泄漏旧缓存

  • allocUnsafe接口分配内存空间速度更优,但有数据安全风险


内存分配

Buffer可直接操作二进制数据类型,这必然要有二进制数据的载体,而在JavaScript里面已经实现了ArrayBuffer对象、TypedArray对象以及DataView对象在ES6的时候纳入了ECMAScript规格里面。其实这些数据结构也被应用在浏览器端,例如File API、WebGL、Canvas、WebSockets等一些API底层都是二进制数据的通信,查看node_buffer.cc源码,Buffer在C++层面分配内存最终也是使用ArrayBuffer对象作为载体,现在先区分一下ArrayBuffer、TypedArray以及DataView三者的区别。

  • ArrayBuffer对象 : 内存中一段原始的二进制数据,可以通过“视图”进行操作。

  • TypedArray对象 : 用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图,比如Buffer里面就使用到Uint8Array(无符号8位整形)数组视图。

  • DataView对象 : 暂时与本文无关不做详细介绍。

简单点而言, 就是Buffer模块使用v8::ArrayBuffer分配一片内存,通过TypedArray中的v8::Uint8Array来去写数据 ,而说道Buffer的内存分配就不得不说Buffer的8KB的问题,对应buffer.js源码里面的处理就是

Buffer.poolSize = 8 * 1024;

function allocate(size)
{
    if(size <= 0 )
        return new FastBuffer();
    if(size < Buffer.poolSize >>> 1 )
        if(size > poolSize - poolOffset)
            createPool();
        var b = allocPool.slice(poolOffset,poolOffset + size);
        poolOffset += size;
        alignPool();
        return b
    } else {
        return createUnsafeBuffer(size);
    }
}

源码直接看来就是以8KB作为界限,如果写入的数据大于8KB一半的话直接则直接去分配内存,如果小于4KB的话则从当前分配池里面判断是否够空间放下当前存储的数据,如果不够则重新去申请8KB的内存空间,把数据存储到新申请的空间里面,如果足够写入则直接写入数据到内存空间里面,下图为其内存分配策略。

Buffer.allocate内存分配策略

如上图,如果当前存储了2KB的数据,后面要存储5KB大小数据的时候分配池判断所需内存空间大于4KB,则会去重新申请内存空间来存储5KB数据并且分配池的当前偏移指针也是指向新申请的内存空间,这时候就之前剩余的6KB(8KB-2KB)内存空间就会被搁置。至于为什么会用8KB作为存储单元分配,这里还没进一步深究。

此外,Buffer单次的内存分配也有限制,而这个限制根据不同操作系统而不同,而这个限制可以看到node_buffer.h里面

static const unsigned int kMaxLength =
    sizeof(int32_t) == sizeof(intptr_t) ? 0x3fffffff : 0x7fffffff;

对于32位的操作系统单次可最大分配的内存为1G,对于64位或者更高的为2G


Buffer与String

Buffer与String两者都可以存储字符串类型的数据,但是,String与Buffer不同,在内存分配上面,String直接使用v8堆存储,不用经过c++堆外分配内存,并且Google也对String进行优化,在实际的拼接测速对比中,String比Buffer快。但是Buffer的出现是为了处理二进制以及其他非Unicode编码的数据,所以在处理非utf8数据的时候需要使用到Buffer来处理。


编码支持

  • ascii - 仅支持7位ASCII数据。

  • utf8 - 多字节编码的Unicode字符

  • utf16le - 2或4个字节,小端编码的Unicode字符

  • base64 - Base64字符串编码

  • binary - 二进制编码。

  • hex - 将每个字节编码为两个十六进制字符。


C小K
117 声望6 粉丝