EasonYou / my-blog

It's my blog recording front-end
2 stars 0 forks source link

【node源码】buffer模块源码阅读 #18

Open EasonYou opened 4 years ago

EasonYou commented 4 years ago

Buffer(缓冲器)

在Node应用中,需要处理网络协议、数据库操作、图片处理、接受上传文件等等,在网络流与文件的操作中,还需要处理大量的二进制文件。自有的字符串远远不能满足这些需求,于是Buffer应运而生

Buffer是二进制数据,字符串与Buffer存在编码关系

Buffer结构

Buffer是一个类Array的对象,但主要应用于操作字节。

模块结构

性能方面由C++实现,非性能相关用JavaScript实现。

Buffer所占用的内存不是通过V8分配,属于堆外内存。这是由于V8的垃圾回收性能影响,将常用的操作对象用更高效和专有的内存分配回收策略来管理。

Buffer分配

从Node 6.0.0开始,抛弃new Buffer,由from方法创建对应的buffer,它的元素是16进制的两位数,即0到255的数值

const str = '深入浅出node.js';
const buf = Buffer.from(str, 'utf-8')

可以通过alloc方法进行内存申请。如果填充内容为undefined,用0填充

const buf = Buffer.alloc(100)

可以通过下标对其进行赋值

const buf = Buffer.alloc(100)
buf[1] = 5
console.log(buf[1]) // 5
buf[2] = 300
console.log(buf[2]) // 44
buf[3] = 3.1415
console.log(buf[3]) // 3
buf[4] = -100
console.log(buf[4])

如上,给元素的赋值如果小于0,就将该值逐次加256,直到得到一个0-255之间的整数。如果得到的数大于255,则逐次减256,直到得到一个0-255之间的整数,如果是小数,只保留整数部分

Buffer的内存分配

Buffer的内存分配不在V8的堆内存中,而是在Node的C++层面实现内存而申请的。

因为处理大量的字节数据,不能采用需要一点内存就向操作系统申请一点内存的方式,这可能会造成大量的内存申请的系统调用,对操作系统有一定压力。

所以,Node是在C++层面申请内存,在JavaScript中分配内存的策略

slab分配机制

slab是一种动态内存管理机制,简而言之,就是一块申请好的固定大小的内存区域

它具有以下三种状态

Node以8kb为界限来区分Buffer是大对象还是小对象,这个值,在Buffer.poolSize中设置。这个值就是每个slab的大小值。在JavaScrip层面以它为单位单元进行内存的分配

  1. 小Buffer对象的分配
  1. 大Buffer对象的分配
    • 当需要超过8KB的Buffer对象,会直接分配一个SlowBuffer(通过allocUnsafeSlow进行分配)对象作为slab单元,将会被这个大Buffer对象独占

以上是在深入浅出Node.js中描述的分配机制,在最新的node实现中,poolSize依然是8KB,但是分配的大小对象会以4KB作为判断的区分

小结

真正的内存是在Node的C++层面提供,JavaScript层面只是使用它

小而频繁的Buffer操作,用slab机制进行预先申请和事后分配,使得JavaScript到操作系统之间不必有过多的内存申请方面的系统调用

大的Buffer而言,直接使用C++层面提供的内存

Buffer与性能

Buffer常用方法介绍

Buffer.alloc(size[, fill[, encoding]])

分配一个大小为size 字节的新 Buffer。 如果fillundefined,则用零填充Buffer

如果size大于 buffer.constants.MAX_LENGTH(在 32 位的架构上,该值是 (2^30)-1 (~1GB)。 在 64 位的架构上,该值是 (2^31)-1 (~2GB)) 或小于 0,则抛出 ERR_INVALID_OPT_VALUE。 如果size为 0,则创建一个零长度的Buffer

const buf = Buffer.alloc(5);
console.log(buf); // 打印: <Buffer 00 00 00 00 00>

const buf = Buffer.alloc(5, 'a');
console.log(buf); // 打印: <Buffer 61 61 61 61 61>

如果指定了encoding会先做decode,再转为buffer

const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');
console.log(buf.toString()); // hello world

Buffer.allocUnsafe(size)

创建一个大小为 size 字节的新 Buffer。实例的底层内存是未初始化的,新创建的Buffer的内容是未知的,可能会包含敏感数据。但是分配内存小于4KB时,则会从一个预分配的Buffer切割出来。可避免垃圾回收机制因创建太多独立的Buffer而过度使用(这里指的就是上面的slab分配机制)。

Buffer.allocUnsafeSlow(size)

allocUnsafe,但是直接申请一个大内存的空间存放,而不走slab的分配机制

Buffer.from

Buffer.poolSize

这是用于缓冲池的预分配的内部 Buffer 实例的大小(以字节为单位)。 该值可以修改。

其他与Array重叠的方法

buffer中很多方法与Array重叠,可直接查阅Array的文档。

Buffer与TypedArray、DataView

在引入TypedArray前,JavaScript语言没有用于读取或操作二进制数据流的机制。因此产生了Buffer类作为Node API的一部分,用于TCP流、文件系统操作、以及其他上下文中与八位字节流进行交互。

它与TypedArray一样,都是描述了一个底层的二进制数据缓冲区(binary data buffer),也就是Array Buffer的一个类数组视图。因为JavaScript不能直接操作二进制数据,需要通过视图,来进行二进制数据的操作。

实际上Buffer就是TypedArray视图中的Unit8Array的node实现

通过使用 TypedArray 对象的 .buffer 属性,可以创建一个与 TypedArray 实例共享相同内存的新 Buffer。

const arr = new Uint16Array(2);

arr[0] = 5000;
arr[1] = 4000;

// 拷贝 `arr` 的内容。
const buf1 = Buffer.from(arr);
// 与 `arr` 共享内存。
const buf2 = Buffer.from(arr.buffer);

console.log(buf1);
// 打印: <Buffer 88 a0>
console.log(buf2);
// 打印: <Buffer 88 13 a0 0f>

arr[1] = 6000;

console.log(buf1);
// 打印: <Buffer 88 a0>
console.log(buf2);
// 打印: <Buffer 88 13 70 17>

当使用 TypedArray 的 .buffer 创建 Buffer 时,也可以通过传入 byteOffset 和 length 参数只使用 TypedArray 的一部分。

const arr = new Uint16Array(20);
// 依然共享TypedArray的内存,裁切16个长度
const buf = Buffer.from(arr.buffer, 0, 16);

console.log(buf.length);
// 打印: 16

ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

TypedArray语法

// 下面代码是语法格式,不能直接运行,
// TypedArray 关键字需要替换为底部列出的构造函数。
new TypedArray(); // ES2017中新增
new TypedArray(length); 
new TypedArray(typedArray); 
new TypedArray(object); 
new TypedArray(buffer [, byteOffset [, length]]); 

// TypedArray 指的是以下的其中之一: 
// 这些方法都会生成二进制数据,只是视图的形式不一样
Int8Array(); // 8 位二进制有符号整数
Uint8Array(); // 8 位无符号整数(超出范围后从另一边界循环)
Uint8ClampedArray(); // 8 位无符号整数(超出范围后为边界值)
Int16Array(); // 16 位二进制有符号整数
Uint16Array(); // 16 位无符号整数
Int32Array(); // 32 位二进制有符号整数
Uint32Array(); // 32 位无符号整数
Float32Array(); // 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567)
Float64Array(); // 64 位 IEEE 浮点数(16 有效数字,如 1.123...15)

Buffer源码阅读

Buffer的构造函数已经不建议使用,可以直接用from方法alloc方法或者allocUnsafe等等。

构造函数

// 构造函数,已经弃用,但是还存在,依然可用,会有warning
function Buffer(arg, encodingOrOffset, length) {
  // Common case.
  if (typeof arg === 'number') {
    // ...
    return Buffer.alloc(arg);
  }
  return Buffer.from(arg, encodingOrOffset, length);
}

alloc方法

用于分配一个全新的Buffer,如果fill为空,用0填充

Buffer.alloc = function alloc(size, fill, encoding) {
  assertSize(size); // 判断size是不是为数字,不是则抛错
  // 如果fill有内容
  if (fill !== undefined && fill !== 0 && size > 0) {
    const buf = createUnsafeBuffer(size); // 先分配一个size大小的buffer
    return _fill(buf, fill, 0, buf.length, encoding); // 然后把内容填充进去
  }
  // fill无内容的情况,直接用FasterBuffer分配
  return new FastBuffer(size);
};

allocUnsafeSlow/createUnsafeBuffer方法

fill不为空那么,其中一个关键方法则是createUnsafeBuffer

其实,这个方法也是allocUnsafeSlow调用的方法

Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) {
  assertSize(size); // 判断size是不是为数字,不是则抛错
  return createUnsafeBuffer(size); // 创建一个大小为 size 字节的新 Buffer
};
// 
const zeroFill = bindingZeroFill || [0];

// 创建一个大小为 size 字节的新 Buffer
function createUnsafeBuffer(size) {
  // ..
  try {
    return new FastBuffer(size); //分配一个size大小的内存
  } finally { /** ... */ }
}

FastBuffer

可以看到,分配空的buffer,都是通过FastBuffer做填充

// FastBuffer直接继承了Uint8Array
// 从这里可以解释,Buffer就是Unit8Array的一个改良版本,更适用于Node.js
class FastBuffer extends Uint8Array {}

FastBuffer.prototype.constructor = Buffer;
Buffer.prototype = FastBuffer.prototype; //Buffer继承了FastBuffer的方法
// addBufferPrototypeMethods是给原型链上添加了非常多的方法
addBufferPrototypeMethods(Buffer.prototype);

function addBufferPrototypeMethods(proto) {
  // 这里只列出一个方法作为示例
  proto.readBigUInt64LE = readBigUInt64LE,
  // ...
}

到这里,可以知道,分配的底层逻辑,就是通过TypedArray中的Uint8Array来实现的

那么,alloc方法会比allocUnsafeSlow更安全的原因,就在于fill方法中

buf.fill/_fill方法

// _fill方法也是fill下调用的方法
Buffer.prototype.fill = function fill(value, offset, end, encoding) {
  return _fill(this, value, offset, end, encoding); // fill中调用_fill方法
};

function _fill(buf, value, offset, end, encoding) {
  // 如果是字符串类型,则需要对其进行encoding处理
  if (typeof value === 'string') {
    // ...
    // 这里只给出重要的点,则是value为空的话,value的填充也用0去做填充
    if (value.length === 0) {
      // If value === '' default to zero.
      value = 0;
    }
    // ...
  } else {
    // 否则,把encoding设为空
    encoding = undefined;
  }
  // 如果没填写偏移量,则默认为0,结束偏移量为最后的长度
  if (offset === undefined) {
    offset = 0;
    end = buf.length;
  } else {
    // 判断offset是否合法,非法则报错
    validateInt32(offset, 'offset', 0);
    // Invalid ranges are not set to a default, so can range check early.
    if (end === undefined) {
      end = buf.length;
    } else {
      // 判断结束偏移量是否合法,非法则报错
      validateInt32(end, 'end', 0, buf.length);
    }
    // 如果偏移量大于结束偏移量,直接返回Buffer实例
    if (offset >= end)
      return buf;
  }
  // 填充buffer,bindingFii是C++模块的buffer实现
  // res是成功标识,0为成功
  const res = bindingFill(buf, value, offset, end, encoding);
  // ...
  return buf;
}

总结

至此,allocallocUnsafeSlow以及fill的方法到此完毕,总结下各自的作用

from方法

alloc方法不同,from方法是直接把value转化成Buffer

Buffer.from = function from(value, encodingOrOffset, length) {
  // 如果是字符串,带上encoding直接做转换
  if (typeof value === 'string')
    return fromString(value, encodingOrOffset);
  // 如果是对象
  if (typeof value === 'object' && value !== null) {
    // 如果是ArrayBuffer,带上offset和拷贝字节数做转换
    if (isAnyArrayBuffer(value))
      return fromArrayBuffer(value, encodingOrOffset, length);
    // 为了保证其值的正确性,需要判断valueOf的值而不是原始值
    const valueOf = value.valueOf && value.valueOf();
    if (valueOf != null &&
        // 如果其valueOf的值不等于原始值,则将其valueOf递归from来做buffer转换
        valueOf !== value &&
        (typeof valueOf === 'string' || typeof valueOf === 'object')) {
      return from(valueOf, encodingOrOffset, length);
    }
    // 转换Object
    const b = fromObject(value);
    if (b)
      return b;
    // 兼容primitive为string的情况
    if (typeof value[SymbolToPrimitive] === 'function') {
      const primitive = value[SymbolToPrimitive]('string');
      if (typeof primitive === 'string') {
        return fromString(primitive, encodingOrOffset);
      }
    }
  }
  // ...
};

from方法兼容了string、Object和ArrayBuffer等情况,接下来分别阐述其对应方法

fromString

fromString先是做了encoding的前置检查动作以及encoding的参数设置,通过fromStringFast进行转换

在这里,做了一个buffer的内存分配中,非常重要的一个点,就是上面提及的slab机制

function fromString(string, encoding) {
  let ops;
  // encoding无效的情况
  if (typeof encoding !== 'string' || encoding.length === 0) {
    if (string.length === 0) // 如果string同样为空,分配一个空的buffer
      return new FastBuffer();
    // 否则,默认encoding为utf8
    ops = encodingOps.utf8;
    encoding = undefined;
  } else {
    // 获取encoding的参数
    ops = getEncodingOps(encoding);
    // ...
  }
  // 通过fromStringFast来转换
  return fromStringFast(string, ops);
}

Buffer.poolSize = 8 * 1024; // poolSize是单个内存块的大小,默认为8KB

// slab机制在这里实现
// 用于创建一个8KB的内存块
function createPool() {
  poolSize = Buffer.poolSize;
  allocPool = createUnsafeBuffer(poolSize).buffer;
  poolOffset = 0;
}
function fromStringFast(string, ops) {
  // 由于encoding的不同,需要特殊地获取确切的字符串长度
  const length = ops.byteLength(string);
  // 如果长度大于4096(>>>是二进制右移,即/2)
  if (length >= (Buffer.poolSize >>> 1))
    // 直接在当前分配可用的内存池去分配
    return createFromString(string, ops.encodingVal);
  // 如果长度大于剩余的内存块大小,则分配一个新的内存块
  if (length > (poolSize - poolOffset))
    createPool();
  // 通过上面的内存池分配记录,来申请获取对应的内存块
  let b = new FastBuffer(allocPool, poolOffset, length);
  // 往对应的内存,写入内容
  const actual = ops.write(b, string, 0, length);
  // 兜底内存块不够的情况,但此情况较少见
  if (actual !== length) {
    // byteLength() may overestimate. That's a rare case, though.
    b = new FastBuffer(allocPool, poolOffset, actual);
  }
  // 更新偏移量
  poolOffset += actual;
  // ...
  return b;
}

fromArrayBuffer

因为ArrayBuffer可以通过Uint8Array直接分配,也没有encoding的限制,所以此方法只做了区间的校验,然后直接通过FastBuffer(Uint8Array)去做buffer转换

function fromArrayBuffer(obj, byteOffset, length) {
  // ...
  // 想要获取的buffer区间小于零的话,报错
  const maxLength = obj.byteLength - byteOffset;
  if (maxLength < 0)
    throw new ERR_BUFFER_OUT_OF_BOUNDS('offset');
  // ...
  return new FastBuffer(obj, byteOffset, length);
}

fromObject

Object分为Uint8ArrayObjectArrayLike三种

// 先看allocate方法
function allocate(size) {
  // 空的话,直接分配空的buffer
  if (size <= 0) {
    return new FastBuffer();
  }
  // 通string,分配小内存buffer用slab机制控制
  if (size < (Buffer.poolSize >>> 1)) {
    if (size > (poolSize - poolOffset))
      createPool();
    const b = new FastBuffer(allocPool, poolOffset, size);
    poolOffset += size;
    alignPool();
    return b;
  }
  // 否则,直接分配大内存
  return createUnsafeBuffer(size);
}

function fromObject(obj) {
  // 如果是Uint8Array的数据格式的话
  if (isUint8Array(obj)) {
    // 先分配内存
    const b = allocate(obj.length);
    // 分配长度为空的话,直接返回
    if (b.length === 0)
      return b;
    // 虽然这个方法名是copy,实质上是做了b.set(obj, start)的动作
    _copy(obj, b, 0, 0, obj.length);
    return b;
  }
  // 如果是数组
  if (obj.length !== undefined || isAnyArrayBuffer(obj.buffer)) {
    // 如果length被各种改些,则是直接分配空的buffer
    if (typeof obj.length !== 'number') {
      return new FastBuffer();
    }
    // 正确的ArrayLike分配
    return fromArrayLike(obj);
  }
  // ..
}

function _copy(source, target, targetStart, sourceStart, sourceEnd) {
  // 上面都是在判断数据的正确性,然后直接调用TypedArray.prototype.set方法
  // set() 方法用于从指定数组中读取值,并将其存储在类型化数组中。
  target.set(source, targetStart);
  return nb;
}

function fromArrayLike(obj) {
  const length = obj.length;
  // 分配内存
  const b = allocate(length);
  // 绑定内容
  for (let i = 0; i < length; i++)
    b[i] = obj[i];
  return b;
}

总结

以上则是buffer的核心方法的源码阅读

Node.js的Buffer,可以说是对TypedArray的Uint8Array一个封装。

但是Buffer在此基础上,对string的encoding做了严格控制和长度的转换,在这基础上与Object类型用同样的slab内存分配机制,对大内存和小内存的分配做了特殊的管理,会预先分配8KB的内存池,大于4KB的数据直接分配需要的内存,小于4KB的数据,判断现有内存池是否有足够空间,有则插入无则重新分配8KB空间。通过这种方式来减少内存支出