深入理解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的DIV随滚动条滚动而滚动的代码
Jul 20 Javascript
JavaScript更改原始对象valueOf的方法
Mar 19 Javascript
jQuery找出网页上最高元素的方法
Mar 20 Javascript
jQuery实现的鼠标经过时变宽的效果(附demo源码)
Apr 28 Javascript
jQuery插件Validation快速完成表单验证的方式
Jul 28 Javascript
JavaScript实现QQ聊天消息展示和评论提交功能
May 22 Javascript
JavaScript学习笔记之惰性函数示例详解
Aug 27 Javascript
jQuery实现判断上传图片类型和大小的方法示例
Apr 11 jQuery
vuex进阶知识点巩固
May 20 Javascript
浅谈layui 表单元素的选中问题
Oct 25 Javascript
24个解决实际问题的ES6代码片段(小结)
Feb 02 Javascript
vue实现简单的登录弹出框
Oct 26 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转换IP地址到真实地址的方法详解
2013/06/09 PHP
PHP禁止个别IP访问网站
2013/10/30 PHP
ThinkPHP采用GET方式获取中文参数查询无结果的解决方法
2014/06/26 PHP
PHP文件读取功能的应用实例
2015/05/08 PHP
解决FireFox下[使用event很麻烦]的问题
2006/11/26 Javascript
JavaScript 开发中规范性的一点感想
2009/06/23 Javascript
JQuery的html(data)方法与&amp;lt;script&amp;gt;脚本块的解决方法
2010/03/09 Javascript
JavaScript中“+”的陷阱深刻理解
2012/12/04 Javascript
jQuery提示插件alertify使用指南
2015/04/21 Javascript
jQuery取消特定的click事件
2016/02/29 Javascript
bootstrap3使用bootstrap datetimepicker日期插件
2017/05/24 Javascript
深入理解JavaScript的async/await
2018/08/05 Javascript
vue + element-ui的分页问题实现
2018/12/17 Javascript
jQuery实现当拉动滚动条到底部加载数据的方法分析
2019/01/24 jQuery
AJAX在JQuery中的应用详解
2019/01/30 jQuery
微信小程序自定义导航栏实例代码
2019/04/05 Javascript
iview的table组件自带的过滤器实现
2019/07/12 Javascript
vue自动添加浏览器兼容前后缀操作
2020/08/13 Javascript
Python中的pass语句使用方法讲解
2015/05/14 Python
python实现用于测试网站访问速率的方法
2015/05/26 Python
Python验证文件是否可读写代码分享
2017/12/11 Python
python 找出list中最大或者最小几个数的索引方法
2018/10/30 Python
Python学习笔记之文件的读写操作实例分析
2019/08/07 Python
图文详解Django使用Pycharm连接MySQL数据库
2019/08/09 Python
Pytorch实现基于CharRNN的文本分类与生成示例
2020/01/08 Python
Python基础之字符串常见操作经典实例详解
2020/02/26 Python
Python用类实现扑克牌发牌的示例代码
2020/06/01 Python
Python数据库封装实现代码示例解析
2020/09/05 Python
详解HTML5.2版本带来的修改
2020/05/06 HTML / CSS
德国机车企业:FC-Moto
2017/10/27 全球购物
Martinelli官方商店:西班牙皮鞋和高跟鞋品牌
2019/07/30 全球购物
食堂厨师岗位职责
2014/08/25 职场文书
群众路线个人整改方案
2014/10/25 职场文书
交警正风肃纪剖析材料
2014/10/29 职场文书
2015年客服工作总结范文
2015/04/02 职场文书
详细谈谈JavaScript中循环之间的差异
2021/08/23 Javascript