Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buffer学习 #37

Open
vivatoviva opened this issue Apr 23, 2019 · 0 comments
Open

Buffer学习 #37

vivatoviva opened this issue Apr 23, 2019 · 0 comments
Labels

Comments

@vivatoviva
Copy link
Owner

是为了提高内存和**硬盘(或其他I/0设备)**之间的数据交换的速度而设计的。缓存是为了提高内存和CPU之间的数据交换速度而产生的

缓冲(buffers)是根据磁盘的读写设计的,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。

Buffer类的引入则让Nodejs有了操作文件流或者网络二进制流的能力

文档部分

Buffer代表缓冲区

在Node中主要分为两种,一种是安全的,一种是不安全的,因为Buffer主要是分成一个内存,如果内存不初始化,则内存中可能存在敏感数据,我们认为这种是不安全的,如果我们分了内存,同时将内存中的数据进行初始化,这样的话我们认为这样是安全的,但是效率比较低,为了在效率和安全之间做一个抉择,Node选择了默认安全方式,使用0进行填充,但是也可以使用unSafe方式创建缓存区,需要自己注意到这一点是不安全的,因为没有初始化内存

一、类方法
  • byteLength
  • compare
  • concat
  • from
  • isBuffer
  • isEncoding
  • poolSize : 预分配Buffer池大小
二、实例方法
  • buf[index]
  • buf.buffer: 得到底层的ArrayBuffer
  • buf.compare
  • buf.copy
  • buf.entries() : [index, byte]形式的迭代器
  • buf.equals : byte内容进行对别
Buffer.from('ABC') === Buffer.from('414243', 'hex')//字节相同
  • buf.fill
  • buf.includes
  • buf.indexOf
  • buf.keys
  • buf.lastIndexOf
  • buf.length : 如果想要改变一个Buffer的长度,我们需要使用slice方法来创建一个新的Buffer
  • 读取
    • buf.readInt8
    • buf.readDoubleBE
    • buf.readDoubleLE
    • buf.readFloatBE
    • buf.readFloatLE
    • buf.readInt8
    • buf.readInt16BE/LE
    • buf.readInt32BE/LE
    • buf.readIntBE/LE
  • buf.slice : 创建一个指向原始的Buffer同一个内存的新的Buffer,使用start和end进行裁剪
  • buf.toJSON : 返回Buffer的JSON格式
  • buf.toString
  • buf.values() : 针对字节的迭代
  • 写入
    • buf.write() : 存在覆盖,如果不指定写入起始位置的时候,返回写入的字节数
    • buf.writeFloatBE/FE
    • buf.writeInt8 直接写8位的数字
    • 。。。
  • buf.INSPECT_MAX_BYTES
三、allocUnsafe和allocUnSafeSlow区别
  • 我们在创建buffer的时候,如果分配的内存小于4kb,则会从一个预分配的 Buffer 切割出来。 这可以避免垃圾回收机制因创建太多独立的 Buffer 而过度使用。

  • 当需要在内存池保留一小块内存时,可以使用 Buffer.allocUnsafeSlow() 创建一个非内存池的 Buffer 并拷贝出来。

四、注意事项
  • Buffer.from(ArrayBuffer, byteOffset, length) 设置了 byteOffset 或创建一个小于 Buffer.poolSizeBuffer 时,底层的 ArrayBuffer 的偏移量并不是从 0 开始

  • 当直接使用 buf.buffer 访问底层的 ArrayBuffer 时, ArrayBuffer 的第一个字节可能并不指向 buf 对象。原因同上

理解部分

一、什么是Buffer

Buffer模块是Node中C++良好结合的一个示例:

Buffer 对象的内存分配不是在V8的堆内存中,而是Node在C++层面进行内存申请,可以理解为在内存中单独开辟了一部分空间,但是使用时分配内存则是由Node层面完成的,释放也是由Node中v8的gc机制自动控制。

  • Buffer对象是为了处理二进制数据的,他是一个构造函数,生成的实例代表了V8引擎分配的一段内存,是一个类似数组的对象,成员为0-255的整数值,代表一个8位的字节,底层的内存分配是在V8 heap的外部,分配内存的大小在Buffer实例创建后不可更改,并且原则上没有大小限制

  • Buffer类是使用八位字节流,所以一个字节的大小是0~255,与C语言中的unsigned char和ES6的Uint8Array的范围是相同的。

  • Buffer类是全局的,直接使用即可,不需要使用require('buffer').Buffer

Buffer实例也是Unit8Array视图。但是它与ECMAScript 2015的TypedArray还有些细微的不同。例如,ArrayBuffer#slice()创建实例的内存是slice方法拷贝的内存,而Buffer#slice()是在Buffer的基础上创建了视图来操作内存,所以Buffer#slice()的效率更高一些,因为没有新生成内存,不会影响原有的内存空间,通过Buffer创建二进制数组时要注意下面几点:

  • TypedArray是拷贝Buffer对象的内存,但不共享内存
  • Buffer对象的数组形式与TypedArry的不同。例如,new Uint32Array(Buffer.from([1,2,3,4]))所创建的Uint32Array是多元素数组[1,2,3,4],而new Uint32Array(TypedArray)创建的是只有一个元素的数组[0x01020304][0x04030201]
二、Buffer和TypesArray的区别
  • 两者可以互相转化,Buffer只支持8位格式的,但是TypesArray中存在各种视图,这些视图可以支持:81632等等位数,在转化过程中主要有两种转化方式:一种是重新建立内存一种是共用以前的开辟的内存空间,两者的区分可以通过调用构造函数传入的值进行区分(看有没有按照下面的流程进行处理)

  • 两个类型的slice方法表现不一致:ArrayBuffer#slice()创建实例的内存是slice方法拷贝的内存,而Buffer#slice()是在Buffer的基础上创建了视图来操作内存,所以Buffer#slice()的效率更高一些,因为没有新生成内存,不会影响原有的内存空间。

image-20190422213357783

通过上面这张图我们可以知道Buffer和TypesArray的底层都是ArrayBuffer,我们针对两者的转化其实也就是按照这个流程来出来的,首先转化为ArrayBuffer,然后通过ArrayBuffer生成Node中的buffer或者ES6中的TypedArray

let arrayBuffer: ArrayBuffer;
const typesBuffer = new Uint16Array(10);
const buffer: Buffer = Buffer.alloc(10);

arrayBuffer = typesBuffer.buffer;
arrayBuffer = buffer.buffer;
1. TypesArray转化为Buffer

通过TypedArray对象的.buffer属性创建的Buffer实例与TypedArray实例共享同一个内存,可以理解为:Buffer类实现了Uint8Array相关API。但Node对Buffer类进行了优化,其更适合在Node.js环境中使用。

const arr = new Uint16Array(2);// arr是TypedArray
const arr8 = new Unit8Array(2);

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

// 拷贝`arr`的内存
const buf1 = Buffer.from(arr);

// 共享`arr`的内存
const buf2 = Buffer.from(arr.buffer);

// 共享`arr8`的内存
const buf3 = Buffer.from(arr8.buffer);
// Prints: <Buffer 88 a0>
console.log(buf1); // 仅仅复制两个元素

// Prints: <Buffer 88 13 a0 0f>
console.log(buf2); // 和arr共享内存

// Prints: <Buffer 88 a0>
console.log(buf3); // 复制arr8的内存空间
2. Buffer 转化为 TypesArray

通过buffer创建TypedArry 注意事项

Buffer对象的数组形式与TypedArry的不同。例如,new Uint32Array(Buffer.from([1,2,3,4]))所创建的Uint32Array是多元素数组[1,2,3,4],而new Uint32Array(TypedArray)创建的是只有一个元素的数组

const buf = Buffer.alloc(10);

const arr1 = new Uint16Array(buf); // 根据buf的迭代器接口生成新的类型

const arr2 = new Uint16Array(buf.buffer); // 共享buf的内存占用

console.log(arr1.length, arr2.length);
三、应用场景
  • 存储需要占用大量内存的数据

Buffer 对象占用的内存空间是不计算在 Node.js 进程内存空间限制上的,所以可以用来存储大对象,但是对象的大小还是有限制的。一般情况下32位系统大约是1G,64位系统大约是2G。

怎么理解流呢?流是数据的集合(与数据、字符串类似),但是流的数据不能一次性获取到,数据也不会全部load到内存中,因此流非常适合大数据处理以及断断续续返回chunk的外部源。流的生产者与消费者之间的速度通常是不一致的,因此需要buffer来暂存一些数据。buffer大小通过highWaterMark参数指定,默认情况下是16Kb。

这里我们有一个应用,如果我们使用Http流传输一个string,Node底层会默认将string转化为Buffer然后进行传输,这时候如果我们能够将这个字符串提出转化成Buffer,并且存放在内存中,每次Http请求的时候直接返回buffer,这样的话就会减少string转化成Buffer的过程,就会提高效率,因此我们每次在写业务的时候,将部分资源提前转化为Buffer,使得性能提升

  • 文件读取

Buffer的使用除了与字符串的转换有性能损耗外,在文件的读取时,有一个highWaterMark设置对性能的影响至关重要。在fs.createReadStream(path, opts)时,我们可以传入一些参数,代码如下:

fs.createReadStream()的工作方式是在内存中准备一段Buffer,然后在fs.read()读取时逐步从磁盘中将字节复制到Buffer中。完成一次读取时,则从这个Buffer中通过slice()方法取出部分数据作为一个小Buffer对象,再通过data事件传递给调用方。如果Buffer用完,则重新分配一个;如果还有剩余,则继续使用。下面为分配一个新的Buffer对象的操作:

var pool;
function allocNewPool(poolSize) {
  pool = new Buffer(poolSize);
  pool.used = 0;
}

在理想的状况下,每次读取的长度就是用户指定的highWaterMark。但是有可能读到了文件结尾,或者文件本身就没有指定的highWaterMark那么大,这个预先指定的Buffer对象将会有部分剩余,不过好在这里的内存可以分配给下次读取时使用。pool是常驻内存的,只有当pool单元剩余数量小于128(kMinPoolSpace)字节时,才会重新分配一个新的Buffer对象。Node源代码中分配新的Buffer对象的判断条件如下所示:

if (!pool || pool.length - pool.used < kMinPoolSpace) { // discard the old pool
  pool = null;
  allocNewPool(this._readableState.highWaterMark);
}

这里与Buffer的内存分配比较类似,highWaterMark的大小对性能有两个影响的点。

  • highWaterMark设置对Buffer内存的分配和使用有一定影响。
  • highWaterMark设置过小,可能导致系统调用次数过多。

文件流读取基于Buffer分配,Buffer则基于SlowBuffer分配,这可以理解为两个维度的分配策略。如果文件较小(小于8 KB),有可能造成slab未能完全使用。

由于fs.createReadStream()内部采用fs.read()实现,将会引起对磁盘的系统调用,对于大文件而言,highWaterMark的大小决定会触发系统调用和data事件的次数。

四、注意事项
  • Buffer是全局global上的一个引用,指向的其实是buffer.Buffer,
  • 我们无法对buffer进行释放,只能使用V8的gc机制进行内存释放
  • Buffer连接实例,如果直接使用+=连接会可能会造成乱码情况,原因是默认调用toString方法
// Nodejs 源码正确读取文件并连接BUffer
var buffers = [];
var nread = 0;

readStream.on('data', function (chunk) {
    buffers.push(chunk);
    nread += chunk.length; // 记录总长度
});

readStream.on('end', function () {
    var buffer = null;
    switch(buffers.length) {
        case 0: buffer = new Buffer(0);
            break;
        case 1: buffer = buffers[0];
            break;
        default:
            buffer = new Buffer(nread);
            for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {
                var chunk = buffers[i];
                chunk.copy(buffer, pos);
                pos += chunk.length;
            }
        break;
    }
});
// 深入浅出NOde buffer连接
var chunks = [];
var size = 0;
res.on('data', function(chunk) {
  chunks.push(chunk);
  size += chunk.length; // 记录总长度
});
res.on('end', function() {
  var buf = Buffer.concat(chunks, size);
  var str = iconv.decode(buf, 'utf8');
  console.log(str);
});

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant