在引入TypedArray之前,JavaScript并没有读取或者操作流或二进制数据数据的机制。而Buffer正是因此被引入Node.js API中,使得JavaScript能够介入TCP字节流、文件操作系统和其他场景并能处理其中的内容。随着TypedArray的普及,Buffer的地位变成了更优化和更适合的Node端Uint8Array API。
简而言之,由于JavaScript没有读取或者操作流或二进制数据数据的机制,
Buffer是在服务器端替JavaScript处理二进制数据流(TCP流和文件流等)。
TypeArray则是在浏览器端替JavaScript处理二进制数据流。
1、buffer的结构
buffer是一个典型的javascript与c++结合的模块,其性能部分用c++实现,非性能部分用javascript来实现。
Buffer所占用的内存不是通过V8分配的,属于堆外内存。由于V8垃圾回收性能的影响,将常用的操作对象用更高效和专有的内存分配回收策略来管理。 由于Buffer常用,Node在进程启动时就已经加载了它,并将其放在全局对象(global) 上。所以在使用Buffer时,无需require()即可使用。
- buffer模块的内部结构:
exports.Buffer = Buffer;
exports.SlowBuffer = SlowBuffer;
exports.INSPECT_MAX_BYTES = 50;
exports.kMaxLength = binding.kMaxLength;
Buffer: 二进制数据容器类,node启动时默认加载
SlowBuffer: 同样也是二进制数据容器类,不过直接进行内存申请
INSPECT_MAX_BYTES: 限制bufObject.inspect()输出的长度
kMaxLength: 一次性内存分配的上限,大小为(2^31 - 1)
- node在启动的时候,就已经加载了Buffer,而其他三个,仍然需要使用require('buffer').***
2、创建Buffer
在 6.0.0 之前的 Node.js 版本中, Buffer 实例是使用 Buffer 构造函数创建的,但是这种方式存在两个问题:
(1)参数复杂: 内存分配,还是内存分配+内容写入,需要根据参数来确定
(2)安全隐患: 分配到的内存可能还存储着旧数据,这样就存在安全隐患
// 本来只想申请一块内存,但是里面却存在旧数据
const buf1 = new Buffer(10) // <Buffer 90 09 70 6b bf 7f 00 00 50 3a>
// 不小心,旧数据就被读取出来了
buf1.toString()
为了解决上述问题,各种形式的 new Buffer() 构造函数都已被弃用,Buffer提供了Buffer.from()
、Buffer.alloc()
、Buffer.allocUnsafe()
、Buffer.allocUnsafeSlow()
四个方法来申请内存:
-
Buffer.from(array)
返回一个新的Buffer
,其中包含提供的八位字节数组的副本。 -
Buffer.from(arrayBuffer[, byteOffset [, length]])
返回一个新的Buffer
,它与给定的ArrayBuffer
共享相同的已分配内存。 -
Buffer.from(buffer)
返回一个新的Buffer
,其中包含给定Buffer
的内容的副本。 -
Buffer.from(string[, encoding])
返回一个新的Buffer
,其中包含提供的字符串的副本。 -
Buffer.alloc(size[, fill[, encoding]])
返回一个指定大小的新建的的已初始化的Buffer
。 此方法比Buffer.allocUnsafe(size)
慢,但能确保新创建的Buffer
实例永远不会包含可能敏感的旧数据。 -
Buffer.allocUnsafe(size)
和Buffer.allocUnsafeSlow(size)
分别返回一个指定大小的新建的未初始化的Buffer
。 由于Buffer
是未初始化的,因此分配的内存片段可能包含敏感的旧数据。
3、内存分配的策略
Buffer采用了如下的管理策略:
3.1 Buffer.from
Buffer.from(value[, ...])
用于申请内存,并将内容写入刚刚申请的内存中,value值是多样的,源码如下:
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可以分成三类:
- ArrayBuffer的实例: ArrayBuffer是ES2015里面引入的,用于在浏览器端直接操作二进制数据,这样Node就与ES2015关联起来,同时,新创建的Buffer与ArrayBuffer内存是共享的
- string: 该方法实现了将字符串转变为Buffer
- Buffer/TypeArray/Array: 会进行值的copy
3.1.1 ArrayBuffer的实例
浏览器、node中对二进制数据的操作相互关联,二者会进行内存的共享。
const b = new ArrayBuffer(4);
const v1 = new Uint8Array(b);
const 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>
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;
}
- 直接内存分配
当字符串所需要的字节大于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);
}
- 借助于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。
const buf1 = new Buffer.from([1,2,3,4,5]);
const buf2 = new Buffer.from(buf1);
console.log(buf1); // <Buffer 01 02 03 04 05>
console.log(buf2); // <Buffer 01 02 03 04 05>
buf1[0] = 16;
buf1[1] = 17;
console.log(buf1); // <Buffer 10 11 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(size[, fill[, encoding]])
-
size
<integer> 新Buffer
的所需长度。 -
fill
<string> | <Buffer> | <integer> 用于预填充新Buffer
的值。默认值:0
。 -
encoding
<string> 如果fill
是一个字符串,则这是它的字符编码。默认值:'utf8'
。
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进行填充。
const buf3 = Buffer.alloc(3, 'a');
console.log(buf3); // <Buffer 61 61 61>
const buf4 = Buffer.alloc(11, 'abCdEfGhIjK', 'base64');
console.log(buf4); // <Buffer 69 b0 9d 11 f1 a1 22 32 69 b0 9d>
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.3 Buffer.allocUnsafe(size)
Buffer.allocUnSafe与Buffer.alloc的区别在于,前者是从采用allocate的策略,尝试从buffer pool中申请内存,而buffer pool是不会进行默认值填充的,所以这种行为是不安全的。使用 Buffer.allocUnsafe()
创建 Buffer
时,如果要分配的内存小于 4KB,则会从一个预分配的 Buffer
切割出来。 这可以避免垃圾回收机制因创建太多独立的 Buffer
而过度使用。
const buf5 = Buffer.allocUnsafe(10);
console.log(buf5);
// 打印: <Buffer 88 63 f7 d5 f6 7f 00 00 00 00>
// (输出的内容是内存的旧数据,每次都不同)
buf5.fill(0);
console.log(buf5);
// 打印: <Buffer 00 00 00 00 00 00 00 00 00 00>
3.4 Buffer.allocUnsafeSlow(size)
Buffer.allocUnsafeSlow有两个大特点: 直接通过c++进行内存分配;不会进行旧值填充。除了这两点与Buffer.allocUnsafe(size)的其他特性一样。
// 从c++模块层面直接申请内存
const buf4 = Buffer.allocUnsafeSlow(5);
console.log(buf4); //<Buffer 01 e4 04 94 22> (输出的内容是内存的旧数据,每次都不同)
参考资源:
https://www.jb51.net/article/115281.htm
https://blog.csdn.net/xiaozhuo_tang/article/details/83113380
https://www.cnblogs.com/copperhaze/p/6232661.html
https://www.cnblogs.com/iicx/p/3859969.html