深入理解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 相关文章推荐
JQuery 表单中textarea字数限制实现代码
Dec 07 Javascript
JQuery浮动DIV提示信息并自动隐藏的代码
Aug 29 Javascript
jQuery使用之标记元素属性用法实例
Jan 19 Javascript
18个非常棒的jQuery代码片段
Nov 02 Javascript
node.js插件nodeclipse安装图文教程
Oct 19 Javascript
第九篇Bootstrap导航菜单创建步骤详解
Jun 21 Javascript
jQuery实现的背景颜色渐变动画效果示例
Mar 24 jQuery
JavaScript获取tr td 的三种方式全面总结(推荐)
Aug 15 Javascript
Angular2监听页面大小变化的解决方法
Oct 09 Javascript
详解vue挂载到dom上会发生什么
Jan 20 Javascript
es6 filter() 数组过滤方法总结
Apr 03 Javascript
vue 清空input标签 中file的值操作
Jul 21 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类分享
2014/02/07 PHP
CodeIgniter框架数据库事务处理的设计缺陷和解决方案
2014/07/25 PHP
PHP中上传多个文件的表单设计例子
2014/11/19 PHP
PHP+MySQL统计该库中每个表的记录数并按递减顺序排列的方法
2016/02/15 PHP
PHP基于redis计数器类定义与用法示例
2018/02/08 PHP
PHP单元测试框架PHPUnit用法详解
2019/01/23 PHP
仿猪八戒网左下角的文字滚动效果
2011/10/28 Javascript
jquery easyui combox一些实用的小方法
2013/12/25 Javascript
jQuery编程中的一些核心方法简介
2015/08/14 Javascript
js 实现省市区三级联动菜单效果
2017/02/20 Javascript
基于 webpack2 实现的多入口项目脚手架详解
2017/06/26 Javascript
详解Vue-cli代理解决跨域问题
2017/09/27 Javascript
浅谈node中的cluster集群
2018/06/02 Javascript
angular 实现的输入框数字千分位及保留几位小数点功能示例
2018/06/19 Javascript
jQuery的Ajax接收java返回数据方法
2018/08/11 jQuery
koa socket即时通讯的示例代码
2018/09/07 Javascript
js实现搜索提示框效果
2020/09/05 Javascript
Python实现的Kmeans++算法实例
2014/04/26 Python
简单介绍Python中的readline()方法的使用
2015/05/24 Python
Python中map和列表推导效率比较实例分析
2015/06/17 Python
Python实现简单的代理服务器
2015/07/25 Python
Python中出现IndentationError:unindent does not match any outer indentation level错误的解决方法
2020/04/18 Python
Python+Tensorflow+CNN实现车牌识别的示例代码
2019/10/11 Python
详解HTML5中的元素与元素
2015/08/17 HTML / CSS
详解三种方式实现平滑滚动页面到顶部的功能
2019/04/23 HTML / CSS
Mio Skincare美国官网:身体紧致及孕期身体护理
2017/03/05 全球购物
MCM英国官网:奢侈皮具制品
2017/04/18 全球购物
输入N,打印N*N矩阵
2012/02/20 面试题
函授毕业自我鉴定
2014/02/04 职场文书
人事专员的岗位职责
2014/03/01 职场文书
市场开发计划书
2014/05/07 职场文书
公司庆典欢迎词
2015/01/26 职场文书
学习雷锋主题班会
2015/08/14 职场文书
2016年“六一儿童节”校园广播稿
2015/12/17 职场文书
二年级作文之动物作文
2019/11/13 职场文书
小型企业的绩效考核制度模板
2019/11/21 职场文书