深入理解Node中的buffer模块


Posted in Javascript onJune 03, 2017

在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的诸多细节十分必要。

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

Javascript 相关文章推荐
动态调用CSS文件的JS代码
Jul 29 Javascript
jQuery创建自定义的选择器用以选择高度大于100的超链接实例
Mar 18 Javascript
jquery实现两个图片渐变切换效果的方法
Jun 25 Javascript
Angularjs在初始化未完毕时出现闪烁问题的解决方法分析
Aug 05 Javascript
AngularJS中$watch和$timeout的使用示例
Sep 20 Javascript
Vue.js中用v-bind绑定class的注意事项
Dec 13 Javascript
ES6解构赋值的功能与用途实例分析
Oct 31 Javascript
vue父组件向子组件(props)传递数据的方法
Jan 02 Javascript
Node 升级到最新稳定版的方法分享
May 17 Javascript
BootStrap中的模态框(modal,弹出层)功能示例代码
Nov 02 Javascript
详解webpack引入第三方库的方式以及注意事项
Jan 15 Javascript
图文详解vue框架安装步骤
Feb 12 Javascript
MvcPager分页控件 适用于Bootstrap
Jun 03 #Javascript
全面解析Node.js 8 重要功能和修复
Jun 02 #Javascript
vue 中自定义指令改变data中的值
Jun 02 #Javascript
Jquery EasyUI $.Parser
Jun 02 #jQuery
BootStrap 动态表单效果
Jun 02 #Javascript
socket.io与pm2(cluster)集群搭配的解决方案
Jun 02 #Javascript
angularjs定时任务的设置与清除示例
Jun 02 #Javascript
You might like
PHP初学者头疼问题总结
2006/10/09 PHP
php checkdate、getdate等日期时间函数操作详解
2010/03/11 PHP
ThinkPHP验证码使用简明教程
2014/03/05 PHP
php实现批量下载百度云盘文件例子分享
2014/04/10 PHP
PHP实现在对象之外访问其私有属性private及保护属性protected的方法
2017/11/20 PHP
PHP实现压缩图片尺寸并转为jpg格式的方法示例
2018/05/10 PHP
javascript 面向对象,实现namespace,class,继承,重载
2009/10/29 Javascript
js querySelector和getElementById通过id获取元素的区别
2012/04/20 Javascript
jQuery实现平滑滚动到指定锚点的方法
2015/03/20 Javascript
JS使用ajax从xml文件动态获取数据显示的方法
2015/03/24 Javascript
JavaScript必看小技巧(必看)
2016/06/07 Javascript
AngularJS基础 ng-init 指令简单示例
2016/08/02 Javascript
JS如何设置元素样式的方法示例
2017/08/28 Javascript
bootstrap table方法之expandRow-collapseRow展开或关闭当前行数据
2020/08/09 Javascript
Vue2 SSR渲染根据不同页面修改 meta
2017/11/20 Javascript
微信小程序WebSocket实现聊天对话功能
2018/07/06 Javascript
vue自定义底部导航栏Tabbar的实现代码
2018/09/03 Javascript
JS浮点数运算结果不精确的Bug解决
2019/08/01 Javascript
JS实现可用滑块滑动的缓动图代码
2019/09/01 Javascript
Python判断操作系统类型代码分享
2014/11/22 Python
Python获取运行目录与当前脚本目录的方法
2015/06/01 Python
Python lambda函数基本用法实例分析
2018/03/16 Python
python+opencv实现高斯平滑滤波
2020/07/21 Python
Python提取频域特征知识点浅析
2019/03/04 Python
python和JavaScript哪个容易上手
2020/06/23 Python
基于CentOS搭建Python Django环境过程解析
2020/08/24 Python
Python colormap库的安装和使用详情
2020/10/06 Python
Python 远程开关机的方法
2020/11/18 Python
美国最受欢迎的童装品牌之一:The Children’s Place
2016/07/23 全球购物
后勤自我鉴定
2013/10/13 职场文书
促销活动策划方案
2014/01/12 职场文书
工商管理专业大学生职业生涯规划范文
2014/03/09 职场文书
乔丹名人堂演讲稿
2014/05/24 职场文书
创先争优承诺书
2015/01/20 职场文书
钱学森电影观后感
2015/06/04 职场文书
Python中for后接else的语法使用
2021/05/18 Python