jsernews 1.0.1

<~>

ts 238 days ago.

原文链接:https://blog.safia.rocks/post/168178033965/node-module-deep-dive-buffer

你好!每周一篇的深入 Node 模块系列如期而至。本周,我将深入 Node 模块中的 Buffer 对象。我承认,当我打开文件进行初始浏览时,我吓了一跳。它有着令人喘息的1599行代码(包括了注释)。但你知道吗?我已经做了足够多的通读,以便我不会被吓到。

在深入研究实际代码之前,快速了解下 Buffer 对象可能有所帮助。Node 中的 Buffer 对象使开发人员能够与二进制数据流进行交互,这对于读取和写入文件系统上的文件等情况特别有用。如果您已经使用过在 fs 模块中的函数,如 fs.createReadStreamfs.createWriteStream,则您已经与 Buffer 打过交道了。举个例子,这里是一个 Buffer,包含了 Node 中单词 “Safia” 的二进制表示。

Buffer.from('Safia')
<Buffer 53 61 66 69 61>

好了,是时候进入细节,看看代码本身。像往常一样,这里是我将浏览的特定版本的 Buffer 类的固定链接。我通常在文件的底部读取代码,以确定特定模块公开的类和 API。下面看看 Buffer 模块导出的内容。

module.exports = exports = {
  Buffer,
  SlowBuffer,
  transcode,
  INSPECT_MAX_BYTES: 50,

  // Legacy
  kMaxLength,
  kStringMaxLength
};

所以它看起来像导出了两个类,一个 Buffer 和一个 SlowBuffer。除了明显的区别,其中一个是慢的,另一个不是,我暂时不确定它们之间的其它不同规格。除了那些类导出外,看起来模块还导出了几个常量和一个函数。

我想要做的第一件事是弄清 SlowBuffer 是什么以及它为什么存在于代码库中。我转到最新版本的 Node 下的 Buffer 文档页面并在 SlowBuffer 类的部分下发现它实际上已被弃用。SlowBuffer 实际上是一个 Unpooled 的 Buffer 对象的变种。未缓冲的 Buffer 是指在内存中尚未初始化缓冲区的 Buffer 实例。

现在我明白了,我开始查看 Buffer 类本身的代码。它提供了很多功能,所以我想把重点放在我在日常开发工作中使用的几个功能上。

首先,我想先看一下 Buffer.from 方法。Buffer.from 允许开发人员从字符串,数组或另一个 Buffer 中创建一个 Buffer 对象。方法定义要求开发人员提供一个 valueencodingOrOffsetlength 参数。当开发人员传递的 value 是数组的情况下,后两个参数分别代表 Buffer 对象将暴露的数组中第一个字节的索引以及Buffer 对象中要显示的总字节数。如果 value 是一个字符串,则第二个参数是字符串的编码(例如 UTF-8 或 ASCII)。

Buffer.from = function from(value, encodingOrOffset, length) {

函数中第一对代码行定义了当 value 的类型是字符串或数组时,该怎么做。该方法相应地调用 fromStringfromArrayBuffer 函数。

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

if (isAnyArrayBuffer(value))
  return fromArrayBuffer(value, encodingOrOffset, length);

我决定先看看 fromString 函数。其函数定义需要一个 string 和一个 encoding,如上所述。

function fromString(string, encoding) {

函数首先处理开发人员提供的参数中潜在的边缘情况。例如,如果用户不提供字符串或编码,该函数将返回一个空的 Buffer。

  if (typeof encoding !== 'string' || encoding.length === 0) {
    if (string.length === 0)
      return new FastBuffer();

如果开发人员不提供编码,则该函数将使用默认编码 UTF-8。length 变量会以 UTF-8 编码来定义字符串中的字节数。

encoding = 'utf8';
length = byteLengthUtf8(string);

接下来的 if 语句检查字符串中的字节长度是否大于 (Buffer.poolSize >>> 1)。对于 (Buffer.poolSize >>> 1) 我有点困惑,所以我做了一些研究。Buffer.poolSize 的值是 8 * 10248192 字节。该数字表示内部 Buffer 对象使用的字节数。然后使用零填充右移将该值向右移1位。零填充右移与“标准”右移(>>)的不同之处在于,当位向右移动时,它不会从左边加入位。结果是,经历零填充右移的每个数字总是正数。本质上,if 语句确定用户试图创建 Buffer 的字符串是否适合默认情况下缓冲区中预先分配的8192个字节。如果是这样,它会相应地加载字符串译注:此处原文有误,当字符串长度大于或等于 Buffer.poolSize 的一半,即4096字节时,会从字符串新建一个 Buffer)。

  if (length >= (Buffer.poolSize >>> 1))
    return createFromString(string, encoding);

另一方面,如果字符串中的字节数大于在缓冲区中预先分配的字节数,它将继续并为该字符串分配更多空间,然后将其存储到缓冲区中译注:如上所述此处原文有误,当字符串的字节数小于 Buffer.pooSize 的一半时,会从预先分配的 allocPool 中返回新的 Buffer,它与 allocPool 共享相同的内存空间)。

if (length > (poolSize - poolOffset))
  createPool();
var b = new FastBuffer(allocPool, poolOffset, length);
const actual = b.write(string, encoding);
if (actual !== length) {
  // byteLength() may overestimate. That's a rare case, though.
  b = new FastBuffer(allocPool, poolOffset, actual);
}
poolOffset += actual;
alignPool();
return b;

接下来,我跳转到用户将数组 Buffer 传递给 Buffer.from 时执行的 fromArrayBuffer 函数。fromArrayBuffer 的函数定义接受数组对象,字节偏移量和数组 Buffer 的长度三个参数。

function fromArrayBuffer(obj, byteOffset, length) {

函数开始于回应潜在的混乱参数值。它首先检查用户是否没有将一个 byteOffset 传递给该函数,在这种情况下,它使用偏移量 0。在其他情况下,该函数确保 byteOffset 是一个正数。

if (byteOffset === undefined) {
  byteOffset = 0;
} else {
  byteOffset = +byteOffset;
  // check for NaN
  if (byteOffset !== byteOffset)
    byteOffset = 0;
}

Buffer 的长度定义为输入数组 Buffer 的长度减去偏移量。

const maxLength = obj.byteLength - byteOffset;

如果 byteOffset 大于输入数组 Buffer 的长度,则函数将抛出错误。

if (maxLength < 0)
    throw new errors.RangeError('ERR_BUFFER_OUT_OF_BOUNDS', 'offset');

最后,该函数执行一些检查以确保新的 ArrayBuffer 的长度是一个在新偏移对象范围内的正数。

if (length === undefined) {
  length = maxLength;
} else {
  // convert length to non-negative integer
  length = +length;
  // Check for NaN
  if (length !== length) {
    length = 0;
  } else if (length > 0) {
    if (length > maxLength)
      throw new errors.RangeError('ERR_BUFFER_OUT_OF_BOUNDS', 'length');
  } else {
    length = 0;
  }

然后,使用修改的 byteOffsetlength 参数从旧的 obj ArrayBuffer 中创建新的 Buffer。

return new FastBuffer(obj, byteOffset, length);

回到 Buffer.from 函数,它会进行更多的检查,以确保用户尝试创建 Buffer 的 value 是有效的。

if (value === null || value === undefined) {
  throw new errors.TypeError(
    'ERR_INVALID_ARG_TYPE',
    'first argument',
    ['string', 'Buffer', 'ArrayBuffer', 'Array', 'Array-like Object'],
    value
  );
}

if (typeof value === 'number')
  throw new errors.TypeError(
    'ERR_INVALID_ARG_TYPE', 'value', 'not number', value
  );

然后函数检查用户传递的 value 是否拥有 valueOf 方法。valueOf 函数在 JavaScript 中的 Object 原型中定义,并为 JavaScript 中的特定对象返回原始类型的值。例如,开发人员可能会创建一个特殊的 Cost 对象来存储对象的价格,并创建一个将价格作为 Number(这是浮点数)返回的 valueOf 函数。从某种意义上说,Buffer.from 方法的这一部分尝试从作为 value 传递给函数的任何对象中提取一个基本类型,并使用它来生成一个新的 Buffer。

const valueOf = value.valueOf && value.valueOf();
if (valueOf !== null && valueOf !== undefined && valueOf !== value)
  return Buffer.from(valueOf, encodingOrOffset, length);

然后函数尝试调用 fromObject 函数并返回由此函数创建的 Buffer(假设它是非空的)。

var b = fromObject(value);
if (b)
  return b;

下一个检查评估传递的值是否定义了 toPrimitive 函数。toPrimitive 函数返回给定 JavaScript 对象的原始值。如果可行的话,Buffer.from 函数尝试从该函数返回的原始值创建一个 Buffer。

if (typeof value[Symbol.toPrimitive] === 'function') {
  return Buffer.from(value[Symbol.toPrimitive]('string'),
                     encodingOrOffset,
                     length);
}

在所有其他情况下,函数抛出一个 TypeError。

throw new errors.TypeError(
  'ERR_INVALID_ARG_TYPE',
  'first argument',
  ['string', 'Buffer', 'ArrayBuffer', 'Array', 'Array-like Object'],
  value
);

所以实质上,Buffer.from 函数将尝试处理字符串或 ArrayBuffer 的值,然后尝试处理类似数组的值,然后尝试提取基本值以创建一个 Buffer,其它情况向用户抛出TypeError。

我想阅读的 Buffer 对象上的下一个函数是 write 函数。Buffer.prototype.write 的函数定义要求开发人员传递要写入的字符串 string,在写入字符串之前跳过的字节数(由 offset 给出),要写入的字节数length ,以及字符串编码 encoding

Buffer.prototype.write = function write(string, offset, length, encoding) {

如果没有给出偏移量,函数会将该字符串写入缓冲区的起始处。

if (offset === undefined) {
  return this.utf8Write(string, 0, this.length);
}

如果未给出 offsetlength,则函数将从 offset 0开始,并使用缓冲区的默认长度。

// Buffer#write(string, encoding)
} else if (length === undefined && typeof offset === 'string') {
  encoding = offset;
  length = this.length;
  offset = 0;
}

最后,如果开发人员同时提供 offsetlength,则该函数确保它们是有效的有限值,并在给定 offset 的情况下正确计算长度。

} else if (isFinite(offset)) {
  offset = offset >>> 0;
  if (isFinite(length)) {
    length = length >>> 0;
  } else {
    encoding = length;
    length = undefined;
  }

  var remaining = this.length - offset;
  if (length === undefined || length > remaining)
    length = remaining;

  if (string.length > 0 && (length < 0 || offset < 0))
    throw new errors.RangeError('ERR_BUFFER_OUT_OF_BOUNDS', 'length', true);
}

在所有其他情况下,该函数假定开发人员正试图使用​​过时版本的 buffer.write API 并抛出错误。

  else {
    // if someone is still calling the obsolete form of write(), tell them.
    // we don't want eg buf.write("foo", "utf8", 10) to silently turn into
    // buf.write("foo", "utf8"), so we can't ignore extra args
    throw new errors.Error(
      'ERR_NO_LONGER_SUPPORTED',
      'Buffer.write(string, encoding, offset[, length])'
    );
  }

一旦函数适当地设置了 offsetlength 变量,它就会根据不同的 encoding 值来确定要执行的操作。如果未给出编码,则默认情况下,buffer.write 方法将采用 UTF-8。

if (!encoding) return this.utf8Write(string, offset, length);

在其他情况下,函数调用适当的 xWrite 函数,其中 x 是编码。我发现有趣的是用于评估潜在编码的 switch 语句检查了 encoding 字符串的长度,然后检查了 encoding 的实际值。实质上,该函数在 switch 语句的不同分支中评估编码为 utf8utf-8 的情况。

  switch (encoding.length) {
    case 4: ...
    case 5: ...
    case 7: ...
    case 8: ...
    case 6: ...
    case 3: ...
  }

还有一些我希望能够通过 Buffer 类了解到的更有趣的功能,但我最终可能会将这些功能放入本博文的第2部分。现在,我会在这里停下来。