资讯专栏INFORMATION COLUMN

通过源码解析 Node.js 中 Buffer 的 8KB 池分配规则和固定位数字的读写

dinfer / 1617人阅读

摘要:且传入数据的大小必须小于。那些固定位数字读写当你在阅读的文档时,看到诸如,这样的时,可能会想到规范中的类提供的那些方法。

在 Node.js 中,Buffer 常常用来存储一些潜在的大体积数据,例如,文件和网络 I/O 所获取来的数据,若不指定编码,则都以 Buffer 的形式来提供,可见其地位非同一般。你或许听说过,Buffer 的创建,是可能会经过内部的一个 8KB 池的,那么具体的规则是什么呢?可以创建一个新 Buffer 实例的 API 那么多,到底哪些 API 会经过,哪些又不会经过呢?或许你在阅读文档时,还看到过许多形如 Buffer#writeUInt32BEBuffer#readUInt32BE 等等这类固定位的数字的读写操作,它们具体是如何实现的呢?

现在让我们一起跟着 Node.js 项目中 lib/buffer.js 中的代码,来一探究竟。

8KB 池分配规则

统计一下,当前版本的 Node.js (v6.0)中可以创建一个新 Buffer 类实例的 API 有:

new Buffer() (已不推荐使用,可能会泄露内存中潜在的敏感信息,具体例子可以看这里)

Buffer.alloc()

Buffer.allocUnsafe()(虽然也有泄露内存中敏感信息的可能,但语义上非常明确)

Buffer.from()

Buffer.concat()

跟着代码追溯,这些 API 最后都会走进两个内部函数中的一个,来创建 Buffer 实例,这两个内部函数分别是 createBuffer()allocate()

// lib/buffer.js
// ...

Buffer.poolSize = 8 * 1024;
var poolSize, poolOffset, allocPool;

function createPool() {
  poolSize = Buffer.poolSize;
  allocPool = createBuffer(poolSize, true);
  poolOffset = 0;
}
createPool();

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;
  }
}

function allocate(size) {
  if (size === 0) {
    return createBuffer(size);
  }
  if (size < (Buffer.poolSize >>> 1)) {
    if (size > (poolSize - poolOffset))
      createPool();
    var b = allocPool.slice(poolOffset, poolOffset + size);
    poolOffset += size;
    alignPool();
    return b;
  } else {
    return createBuffer(size, true);
  }
}

通过代码可以清楚的看到,若最后创建时,走的是 createBuffer() 函数,则不经过 8KB 池,若走 allocate() 函数,当传入的数据大小小于 Buffer.poolSize 有符号右移 1 位后的结果(相当于将该值除以 2 再向下取整,在本例中,为 4 KB),才会使用到 8KB 池(若当前池剩余空间不足,则创建一个新的,并将当前池指向新池)。

那么现在让我们来看看,这些 API 都走的是哪些方法:

// lib/buffer.js
// ...

Buffer.alloc = function(size, fill, encoding) {
  // ...
  return createBuffer(size);
};

Buffer.allocUnsafe = function(size) {
  assertSize(size);
  return allocate(size);
};

Buffer.from = function(value, encodingOrOffset, length) {
  // ...
  if (value instanceof ArrayBuffer)
    return fromArrayBuffer(value, encodingOrOffset, length);

  if (typeof value === "string")
    return fromString(value, encodingOrOffset);

  return fromObject(value);
};

function fromArrayBuffer(obj, byteOffset, length) {
  byteOffset >>>= 0;

  if (typeof length === "undefined")
    return binding.createFromArrayBuffer(obj, byteOffset);

  length >>>= 0;
  return binding.createFromArrayBuffer(obj, byteOffset, length);
}

function fromString(string, encoding) {
  // ...
  if (length >= (Buffer.poolSize >>> 1))
    return binding.createFromString(string, encoding);

  if (length > (poolSize - poolOffset))
    createPool();
  var actual = allocPool.write(string, poolOffset, encoding);
  var b = allocPool.slice(poolOffset, poolOffset + actual);
  poolOffset += actual;
  alignPool();
  return b;
}

Buffer.concat = function(list, length) {
  // ...
  var buffer = Buffer.allocUnsafe(length);
  // ...
  return buffer;
};

挺一目了然的,让我们来总结一下,当在以下情况同时都成立时,创建的新的 Buffer 类实例才会经过内部 8KB 池:

通过 Buffer.allocUnsafeBuffer.concatBuffer.from(参数不为一个 ArrayBuffer 实例)和 new Buffer(参数不为一个 ArrayBuffer 实例)创建。

传入的数据大小不为 0 。

且传入数据的大小必须小于 4KB 。

那些固定位数字读写 API

当你在阅读 Buffer 的文档时,看到诸如 Buffer#writeUInt32BEBuffer#readUInt32BE 这样的 API 时,可能会想到 ES6 规范中的 DateView 类提供的那些方法。其实它们做的事情十分相似,Node.js 项目中甚至还有将这些 API 的底层都替换成原生的 DateView 实例来操作的 PR ,但该 PR 目前已被标记为 stalled ,具体原因大致是:

没有显著的性能提升。

会在实例被初始化后又增加新的属性。

noAssert 参数将会失效。

先不管这个 PR ,其实,这些读写操作,若数字的精度在 32 位以下,则对应方法都是由 JavaScript 实现的,十分优雅,利用了 TypeArray 下那些类(Buffer 中使用的是 Uint8Array)的实例中的元素,在位溢出时,会抛弃溢出位的机制。以 writeUInt32LEwriteUInt32BE (LE 和 BE 即小端字节序和大端字节序,可以参阅这篇文章)为例,一个 32 位无符号整数需要 4 字节存储,大端字节序时,则第一个元素为直接将传入的 32 位整数无符号右移 24 位,获取到原最左的 8 位,抛弃当下左边的所有位。以此类推,第二个元素为无符号右移 16 位,第三个元素为 8 位,第四个元素无需移动(小端字节序则相反):

Buffer.prototype.writeUInt32BE = function(value, offset, noAssert) {
  value = +value;
  offset = offset >>> 0;
  if (!noAssert)
    checkInt(this, value, offset, 4, 0xffffffff, 0);
  this[offset] = (value >>> 24);
  this[offset + 1] = (value >>> 16);
  this[offset + 2] = (value >>> 8);
  this[offset + 3] = value;
  return offset + 4;
};

读操作与之对应,使用了无符号左移后腾出空位再进行 | 操作合并:

Buffer.prototype.readUInt32BE = function(offset, noAssert) {
  offset = offset >>> 0;
  if (!noAssert)
    checkOffset(offset, 4, this.length);

  return (this[offset] * 0x1000000) +
      ((this[offset + 1] << 16) |
      (this[offset + 2] << 8) |
      this[offset + 3]);
};

其中的 (this[offset] * 0x1000000) + 相当于 this[offset] << 24 |

最后

参考:

https://github.com/nodejs/node/blob/master/lib/buffer.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79349.html

相关文章

  • Node闲谈之Buffer

    摘要:闲谈系列不涉及具体的讲解,只会勾勾画画一些自己认为比较重要的特性。我们一般认为用两个字节位表示,并且完全囊括了字符集。将其转换成进制就是只是表示它们是码。三的读取和写入相关重要的只有能够读写,才能够显示其存在的价值。 原文地址:http://www.cnblogs.com/DeanCh... 在刚接触Nodejs的时候,有些概念总让学前端的我感到困惑(虽然大学的时候也是在搞后端,世界上...

    Godtoy 评论0 收藏0
  • Node.js 缓冲区(Buffer)究竟是什么?

    摘要:在创建时大小已经被确定且是无法调整的,在内存分配这块是由层面提供而不是具体后面会讲解。在这里不知道你是否认为这是很简单的但是上面提到的一些关键词二进制流缓冲区,这些又都是什么呢下面尝试做一些简单的介绍。 showImg(https://segmentfault.com/img/remote/1460000019894717?w=1280&h=850); 多数人都拥有自己不了解的能力和机...

    scwang90 评论0 收藏0
  • 认识node核心模块--从Buffer、Stream到fs

    摘要:端输入数据到端,对就是输入流,得到的对象就是可读流对就是输出端得到的对象是可写流。在中,这四种流都是的实例,它们都有事件,可读流具有监听数据到来的事件等,可写流则具有监听数据已传给低层系统的事件等,和都同时实现了和的事件和接口。 原文地址在我的博客 node中的Buffer和Stream会给刚接触Node的前端工程师们带来困惑,原因是前端并没有类似概念(or 有我们也没意识到)。然而,...

    TANKING 评论0 收藏0
  • Netty ByteBuf

    摘要:主要用来检测对象是否泄漏。子类实现相关的方法是否支持数组,判断缓冲区的实现是否基于字节数组如果缓冲区的实现基于字节数组,返回字节数组 ByteBuf ByteBuf需要提供JDK ByteBuffer的功能(包含且不限于),主要有以下几类基本功能: 7种Java基础类型、byte[]、ByteBuffer(ByteBuf)的等的读写 缓冲区自身的copy和slice 设置网络字节序 ...

    meislzhua 评论0 收藏0
  • Node.js 高级进阶之 fs 文件模块学习

    摘要:回调函数提供两个参数和,表示有没有错误发生,是文件内容。文件关闭第一个参数文件时传递的文件描述符第二个参数回调函数回调函数有一个参数错误,关闭文件后执行。 showImg(//img.mukewang.com/5d3f890d0001836113660768.jpg); 人所缺乏的不是才干而是志向,不是成功的能力而是勤劳的意志。 —— 部尔卫 文章同步到github博客:https:/...

    verano 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<