欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

深入理解Node中的buffer模块

程序员文章站 2022-04-10 10:55:30
在node、es2015出现之前,前端工程师只需要进行一些简单的字符串或dom操作就可以满足业务需要,所以对二进制数据是比较陌生。node出现以后,前端面对的技术场景发生了...

在node、es2015出现之前,前端工程师只需要进行一些简单的字符串或dom操作就可以满足业务需要,所以对二进制数据是比较陌生。node出现以后,前端面对的技术场景发生了变化,可以深入到网络传输、文件操作、图片处理等领域,而这些操作都与二进制数据紧密相关。

node里面的buffer,是一个二进制数据容器,数据结构类似与数组,数组里面的方法在buffer都存在(slice操作的结果不一样)。下面就从源码(v6.0版本)层面分析,揭开buffer操作的面纱。

1. buffer的基本使用

在node 6.0以前,直接使用new buffer,但是这种方式存在两个问题:

  1. 参数复杂: 内存分配,还是内存分配+内容写入,需要根据参数来确定
  2. 安全隐患: 分配到的内存可能还存储着旧数据,这样就存在安全隐患
// 本来只想申请一块内存,但是里面却存在旧数据
const buf1 = new buffer(10) // <buffer 90 09 70 6b bf 7f 00 00 50 3a>
// 不小心,旧数据就被读取出来了
buf1.tostring() // '�\tpk�\u0000\u0000p:'

为了解决上述问题,buffer提供了buffer.frombuffer.allocbuffer.allocunsafebuffer.allocunsafeslow四个方法来申请内存。

// 申请10个字节的内存
const buf2 = buffer.alloc(10) // <buffer 00 00 00 00 00 00 00 00 00 00>
// 默认情况下,用0进行填充
buf2.tostring() //'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'

// 上述操作就相当于
const buf1 = new buffer(10);
buf.fill(0);
buf.tostring(); // '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'

2. buffer的结构

buffer是一个典型的javascript与c++结合的模块,其性能部分用c++实现,非性能部分用javascript来实现。

深入理解Node中的buffer模块

下面看看buffer模块的内部结构:

exports.buffer = buffer;
exports.slowbuffer = slowbuffer;
exports.inspect_max_bytes = 50;
exports.kmaxlength = binding.kmaxlength;

buffer模块提供了4个接口:

  1. buffer: 二进制数据容器类,node启动时默认加载
  2. slowbuffer: 同样也是二进制数据容器类,不过直接进行内存申请
  3. inspect_max_bytes: 限制bufobject.inspect()输出的长度
  4. kmaxlength: 一次性内存分配的上限,大小为(2^31 - 1)

其中,由于buffer经常使用,所以node在启动的时候,就已经加载了buffer,而其他三个,仍然需要使用require('buffer').***。

关于buffer的内存申请、填充、修改等涉及性能问题的操作,均通过c++里面的node_buffer.cc来实现:

// c++里面的node_buffer
namespace node {
 bool zero_fill_all_buffers = false;
 namespace buffer {
  ...
 }
}
node_module_context_aware_builtin(buffer, node::buffer::initialize) 

3. 内存分配的策略

node中buffer内存分配太过常见,从系统性能考虑出发,buffer采用了如下的管理策略。

 深入理解Node中的buffer模块

3.1 buffer.from

buffer.from(value, ...)用于申请内存,并将内容写入刚刚申请的内存中,value值是多样的,buffer是如何处理的呢?让我们一起看看源码:

buffer.from = function(value, encodingoroffset, length) {
 if (typeof value === 'number')
  throw new typeerror('"value" argument must not be a number');

 if (value instanceof arraybuffer)
  return fromarraybuffer(value, encodingoroffset, length);

 if (typeof value === 'string')
  return fromstring(value, encodingoroffset);

 return fromobject(value);
};

value可以分成三类:

  1. arraybuffer的实例: arraybuffer是es2015里面引入的,用于在浏览器端直接操作二进制数据,这样node就与es2015关联起来,同时,新创建的buffer与arraybuffer内存是共享的
  2. string: 该方法实现了将字符串转变为buffer
  3. buffer/typearray/array: 会进行值的copy

3.1.1 arraybuffer的实例

node v6与时俱进,将浏览器、node中对二进制数据的操作关联起来,同时二者会进行内存的共享。

var b = new arraybuffer(4);
var v1 = new uint8array(b);
var buf = buffer.from(b)
console.log('first, typearray: ', v1) // first, typearray: uint8array [ 0, 0, 0, 0 ]
console.log('first, buffer: ', buf) // first, buffer: <buffer 00 00 00 00>
v1[0] = 12
console.log('second, typearray: ', v1) // second, typearray: uint8array [ 12, 0, 0, 0 ]
console.log('second, buffer: ', buf) // second, buffer: <buffer 0c 00 00 00>

在上述操作中,对arraybuffer的操作,引起buffer值的修改,说明二者在内存上是同享的,再从源码层面了解下这个过程:

// buffer.js buffer.from(arraybuffer, ...)进入的分支:
function fromarraybuffer(obj, byteoffset, length) {
 byteoffset >>>= 0;

 if (typeof length === 'undefined')
  return binding.createfromarraybuffer(obj, byteoffset);

 length >>>= 0;
 return binding.createfromarraybuffer(obj, byteoffset, length);
}
// c++ 模块中的node_buffer:
void createfromarraybuffer(const functioncallbackinfo<value>& args) {
 ...
 local<arraybuffer> ab = args[0].as<arraybuffer>();
 ...
 local<uint8array> ui = uint8array::new(ab, offset, max_length);
 ...
 args.getreturnvalue().set(ui);
}

3.1.2 string

可以实现字符串与buffer之间的转换,同时考虑到操作的性能,采用了一些优化策略避免频繁进行内存分配:

function fromstring(string, encoding) {
 ...
 var length = bytelength(string, encoding);
 if (length === 0)
  return buffer.alloc(0);
 // 当字符所需要的字节数大于4kb时: 直接进行内存分配
 if (length >= (buffer.poolsize >>> 1))
  return binding.createfromstring(string, encoding);
 // 当字符所需字节数小于4kb: 借助allocpool先申请、后分配的策略
 if (length > (poolsize - pooloffset))
  createpool();
 var actual = allocpool.write(string, pooloffset, encoding);
 var b = allocpool.slice(pooloffset, pooloffset + actual);
 pooloffset += actual;
 alignpool();
 return b;
}

a. 直接内存分配

当字符串所需要的字节大于4kb时,如何还从8kb的buffer pool中进行申请,那么就可能存在内存浪费,例如:

poolsize - pooloffset < 4kb: 这样就要重新申请一个8kb的pool,刚才那个pool剩余空间就会被浪费掉

看看c++是如何进行内存分配的:

// c++
void createfromstring(const functioncallbackinfo<value>& args) {
 ...
 local<object> buf;
 if (new(args.getisolate(), args[0].as<string>(), enc).tolocal(&buf))
  args.getreturnvalue().set(buf);
}

b. 借助于pool管理

用一个pool来管理频繁的行为,在计算机中是非常常见的行为,例如http模块中,关于tcp连接的建立,就设置了一个tcp pool。

function fromstring(string, encoding) {
 ...
 // 当字符所需字节数小于4kb: 借助allocpool先申请、后分配的策略
 // pool的空间不够用,重新分配8kb的内存
 if (length > (poolsize - pooloffset))
  createpool();
 // 在buffer pool中进行分配
 var actual = allocpool.write(string, pooloffset, encoding);
 // 得到一个内存的视图view, 特殊说明: slice不进行copy,仅仅创建view
 var b = allocpool.slice(pooloffset, pooloffset + actual);
 pooloffset += actual;
 // 校验pooloffset是8的整数倍
 alignpool();
 return b;
}

// pool的申请
function createpool() {
 poolsize = buffer.poolsize;
 allocpool = createbuffer(poolsize, true);
 pooloffset = 0;
}
// node加载的时候,就会创建第一个buffer pool
createpool();
// 校验pooloffset是8的整数倍
function alignpool() {
 // ensure aligned slices
 if (pooloffset & 0x7) {
  pooloffset |= 0x7;
  pooloffset++;
 }
}

3.1.3 buffer/typearray/array

可用从一个现有的buffer、typearray或array中创建buffer,内存不会共享,仅仅进行值的copy。

var buf1 = new buffer([1,2,3,4,5]);
var buf2 = new buffer(buf1);
console.log(buf1); // <buffer 01 02 03 04 05>
console.log(buf2); // <buffer 01 02 03 04 05>
buf1[0] = 16
console.log(buf1); // <buffer 10 02 03 04 05>
console.log(buf2); // <buffer 01 02 03 04 05>

上述示例就证明了buf1、buf2没有进行内存的共享,仅仅是值的copy,再从源码层面进行分析:

function fromobject(obj) {
 // 当obj为buffer时
 if (obj instanceof buffer) {
  ...
  const b = allocate(obj.length);
  obj.copy(b, 0, 0, obj.length);
  return b;
 }
 // 当obj为typearray或array时
 if (obj) {
  if (obj.buffer instanceof arraybuffer || 'length' in obj) {
   ...
   return fromarraylike(obj);
  }
  if (obj.type === 'buffer' && array.isarray(obj.data)) {
   return fromarraylike(obj.data);
  }
 }

 throw new typeerror(kfromerrormsg);
}
// 数组或类数组,逐个进行值的copy
function fromarraylike(obj) {
 const length = obj.length;
 const b = allocate(length);
 for (var i = 0; i < length; i++)
  b[i] = obj[i] & 255;
 return b;
}

3.2 buffer.alloc

buffer.alloc用于内存的分配,同时会对内存的旧数据进行覆盖,避免安全隐患的产生。

buffer.alloc = function(size, fill, encoding) {
 ...
 if (size <= 0)
  return createbuffer(size);
 if (fill !== undefined) {
  ...
  return typeof encoding === 'string' ?
    createbuffer(size, true).fill(fill, encoding) :
    createbuffer(size, true).fill(fill);
 }
 return createbuffer(size);
};
function createbuffer(size, nozerofill) {
 flags[knozerofill] = nozerofill ? 1 : 0;
 try {
  const ui8 = new uint8array(size);
  object.setprototypeof(ui8, buffer.prototype);
  return ui8;
 } finally {
  flags[knozerofill] = 0;
 }
}

上述代码有几个需要注意的点:

3.2.1 先申请后填充

alloc先通过createbuffer申请一块内存,然后再进行填充,保证申请的内存全部用fill进行填充。

var buf = buffer.alloc(10, 11);
console.log(buf); // <buffer 0b 0b 0b 0b 0b 0b 0b 0b 0b 0b>

3.2.2 flags标示

flags用于标识默认的填充值是否为0,该值在javascript中设置,在c++中进行读取。

// js
const binding = process.binding('buffer');
const bindingobj = {};
...
binding.setupbufferjs(buffer.prototype, bindingobj);
...
const flags = bindingobj.flags;
const knozerofill = 0;
// c++
void setupbufferjs(const functioncallbackinfo<value>& args) {
 ...
 local<object> bobj = args[1].as<object>();
 ...
 bobj->set(string::newfromutf8(env->isolate(), "flags"),
  uint32array::new(array_buffer, 0, fields_count));
}

3.2.3 uint8array

uint8array是es2015 typearray中的一种,可以在浏览器中创建二进制数据,这样就把浏览器、node连接起来。

3.3 buffer.allocunsafe

buffer.allocunsafe与buffer.alloc的区别在于,前者是从采用allocate的策略,尝试从buffer pool中申请内存,而buffer pool是不会进行默认值填充的,所以这种行为是不安全的。

buffer.allocunsafe = function(size) {
 assertsize(size);
 return allocate(size);
};

3.4 buffer.allocunsafeslow

buffer.allocunsafeslow有两个大特点: 直接通过c++进行内存分配;不会进行旧值填充。

buffer.allocunsafeslow = function(size) {
 assertsize(size);
 return createbuffer(size, true);
};

4. 结语

字符串与buffer之间存在较大的差距,同时二者又存在编码关系。通过node,前端工程师已经深入到网络操作、文件操作等领域,对二进制数据的操作就显得非常重要,因此理解buffer的诸多细节十分必要。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。