IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Node.js模块之Buffer

    C小K发表于 2017-03-29 16:13:58
    love 0

    简言

    在没有出现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 - 将每个字节编码为两个十六进制字符。



沪ICP备19023445号-2号
友情链接