在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来实现。

下面看看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采用了如下的管理策略。

 

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

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

深入理解Node中的buffer模块的更多相关文章

  1. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  3. Node.js调试技术总结分享

    Node.js是一个可以快速构建网络服务及应用的平台。该平台的构建是基于Chrome's JavaScript runtime,也就是说,实际上它是对Google V8引擎(应用于Google Chrome浏览器)进行了封装。 今天介绍Node.js调式目前有几种技术,需要的朋友可以参考下。

  4. node.js实现http服务器与浏览器之间的内容缓存操作示例

    这篇文章主要介绍了node.js实现http服务器与浏览器之间的内容缓存操作,结合实例形式分析了node.js http服务器与浏览器之间的内容缓存原理与具体实现技巧,需要的朋友可以参考下

  5. 教你如何使用node.js制作代理服务器

    本文介绍了如何使用node.js制作代理服务器,图文并茂,十分的详细,代码很简洁易懂,这里推荐给大家。

  6. node.js中的fs.openSync方法使用说明

    这篇文章主要介绍了node.js中的fs.openSync方法使用说明,本文介绍了fs.openSync方法说明、语法、接收参数、使用实例和实现源码,需要的朋友可以参考下

  7. Node.js+ELK日志规范的实现

    这篇文章主要介绍了Node.js+ELK日志规范的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  8. node.js爬虫框架node-crawler初体验

    这篇文章主要介绍了node.js爬虫框架node-crawler的相关资料,帮助大家利用node.js进行爬虫,感兴趣的朋友可以了解下

  9. node.js中的fs.existsSync方法使用说明

    这篇文章主要介绍了node.js中的fs.existsSync方法使用说明,本文介绍了fs.existsSync方法说明、语法、接收参数、使用实例和实现源码,需要的朋友可以参考下

  10. 说说如何利用 Node.js 代理解决跨域问题

    这篇文章主要介绍了Node.js代理解决跨域问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部