浅谈Node中的Buffer

245 阅读6分钟

Buffer

Buffer()已经弃用,构造Buffer对象时使用Buffer.alloc()、Buffer.allocUnsafe()或Buffer.from()方法
Buffer 实例一般用于表示编码字符的序列,比如 UTF-8 、 UCS2 、 Base64 、或十六进制编码的数据。
Buffer是一个像Array的对象,主要用于操作字节。
Buffer是一个典型的JavaScript于C++结合的模块,它将性能相关的部分用C++实现,非性能部分用JavaScript实现。

1.基础使用

// 创建两个buffer 大小分别为10和20
const buffer1 = Buffer.alloc(10)
const buffer2 = Buffer.alloc(20)
// 修改buffer中的一个元素,且值不在0~255区间中
buffer1[5] = -100
buffer2[8] = 300

console.log(buffer1)
// 输出:<Buffer 00 00 00 00 00 9c 00 00 00 00>

console.log(buffer2)
// 输出:<Buffer 00 00 00 00 00 00 00 00 2c 00 00 00 00 00 00 00 00 00 00 00>

// 拼接buffer1和buffer2
const buffer3 = Buffer.concat([buffer1, buffer2])
// 输出:<Buffer 00 00 00 00 00 9c 00 00 00 00 00 00 00 00 00 00 00 00 2c 00 00 00 00 00 00 00 00 00 00 00>
console.log(buffer3)

2.Buffer对象

Buffer对象类似于数组,它的元素为16禁止的两位数,即0到255的十进制数值。不同编码的字符串占用的元素个数各不相同。 中文在UTF-8下占用三个元素,字符和半角标点符号占用1个元素。
给Buffer对象的元素赋值如果小于0,会将该值主次加256,直到得到0~255之间的整数; 如果得到的数值大于255,会逐次减255,直到得到0~255之间的整数。

3.Buffer内存分配

  • Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请,C++层面申请内存,JavaScript中分配内存的策略。
    • 为了高效使用申请的内存,Node采用slab分配机制。slab就是一块申请号的固定大小的内存区域具有三种状态:
      • full: 完全分配状态
      • partial:部分分配状态
      • empty:没有被分配状态
  • Node以8KB为界限来区分Buffer是大对象还是小对象 - Buffer.poolSize = 8 * 1024

1.分配小Buffer对象

  • 如果指定的Buffer大小小于8KB,Node会按照小对象的方式进行分配

Buffer分配过程中主要使用一个全局变量pool作为中间件处理对象,处于分配状态的slab单元指向它。以下为构造一个新的slab单元。

   var pool
   function allocPool () {
     pool = new SlowBuffer(Buffer.poolSize)
     pool.user = 0
   }

目前pool处于empty(没有被分配)状态,构造小Buffer对象时代码为Buffer.alloc(byteLength),这次构造会检查pool对象,如果pool没有 被创建或pool的剩余可用空间小于新Buffer对象的大小,将会创建一个新的slab单元指向它: if (!pool || pool.length - pool.used < this.length) allocPool() 同时当前Buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置开始使用的

  this.parent = pool
  this.offset = pool.used
  this.used += this.length

这个时候slab的状态为partial(部分分配)。
当再次创建Buffer对象时,构造过程将会判断这个slab剩余空间是否足够。如果足够使用剩余空间并更新slab的分配状态。
如果slab剩余空间不够,将会构造一个新的slab,原slab中剩余的空间将会造成浪费。

  • 由于同一个slab可能分配给多个Buffer对象使用,只有这些小Buffer对象在作用域释放并全部回收时,slab的8KB空间才会被回收

2.分配大Buffer对象

如果需要超过8KB的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个单元被这个大Buffer对象独占。 this.parent = new SlowBuffer(this.length);this.offset = 0
小Buffer对象都是JavaScript层面的,能够被V8垃圾回收标记回收。内部的parent属性指向的SlowBuffer对象却来自于Node自身 C++中的定义,是C++层面上的Buffer对象,所用内存不再V8中。

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

Buffer拼接

   const fs = require('fs')
    const { resolve } = require('path')
   const readStream = fs.createReadStream(resolve(__dirname, './test.txt'))
   let data = ''
   readStream.on('data', chunk => {
     data += chunk
   })
   readStream.on('end', () => {
     console.log(data)
   })

data += chunk这句代码中隐藏了toString()操作,它等价于data = data.toString() += chunk.toString(),乱码的问题就是这样产生的。 比如将文件可读流的每次读取的Buffer长度限制为11,fs.createReadStream(path, { highWaterMatk: 11 })
床前明月光,疑是地上霜。举头望明月,低头思故乡。
以这行文本作为读取目标,将会产生乱码,因为中文是宽字节字符,解析buffer的时候是以3个元素为单元解析的,当解析到月即第10个 元素的时候,没有对应的字符,便会解析为?,可读流触发第一次触发data的时候一共11个buffer的元素,前9个可以正常解析,10、11 将会背解析为两个问号,第二次触发data的时候,第一个元素也没有匹配的字符,会被解析为?(月字将被解析为???),后面依次类推。
解决乱码(setEncoding与string_decoder):

  const readStream = fs.createReadStream(resolve(__dirname, './test.txt'))
  readStream.setEncoding('utf-8') // 乱码将不再产生

使用setEncoding作用是让data时间中传递的不再是Buffer对象,而是编码后的字符串,但是无论如何编码触发data的次数依旧相同,意味着设置 encoding并未改变按段读取的事实。
实际上在调用setEncoding时,可读流对象在内部设置了decoder对象,每次data事件通过该decoder对象进行Buffer到字符串的解码,然后传递给调用者。 decoder对象得到编码后,直到宽字节在utf-编码下是以三个字节的方式存储的,所以第一次write是,只出现前9个字节码形成的字符,“月”字的前两个字节 被保留在decoder实例内部。第二次write时,会将着这2个剩余字节和后续11个字节组合在一起,再次用3的倍数字节进行转码。狱是乱码的问题被解决。

正确拼接Buffer

通过setEncoding方法不可否认能解决大部分得乱码问题,但不能从根本上解决这个问题

淘汰掉setEncoding方法后,剩下的解决方案是将多个小Buffer对象拼接为一个Buffer对象,然后通过iconv_lite一类的模块来转码。

  const readStream = fs.createReadStream(resolve(__dirname, './test.txt'))
  const chunks = []
  let size = 0
  readStream.on('data', chunk => {
    chunks.push(chunk)
    size += chunk.length
  })
  res.on('end', () => {
    // 把多个缓冲区的内容放到一个缓冲区中
    const buf = Buffer.concat(chunks, size)
    const str = iconv_decode(buf, 'utf-8')
    console.log(str)
  })
  function iconv_decode (buffer, decoding) {}

// Buffer.concat的实现
class Buffer {
  static concat (list, length) {
    if (!Array.isArray(list)) {
      throw new Error('Usage: Buffer.concat(list, [length])')
    }
    if (list.length === 0) {
      return Buffer.alloc(0)
    } else if (list.length === 1) {
      // list中只有一个Buffer,直接返回该Buffer对象
      return list[0]
    }
    // 如果length值不合法,则初始化并重新计算length
    if (typeof length !== 'number') {
      length = 0
      for (let i = 0; i < list.length; i++) {
        const buf = list[i]
        length += buf.length
      }
    }
    // 重新构造一个Buffer对象
    const buffer = Buffer.alloc(length)
    let pos = 0
    for (let i = 0; i < list.length; i++) {
      const buf = list[i]
      // 将所有Buffer片段复制到新构造的Buffer中
      buf.copy(buffer, pos) // copy:从输入缓冲区赋值到另一个缓冲区
      pos += buf.length
    }
    return buffer
  }
}

Buffer与性能

  • 通过预先转换静态内容为Buffer对象,可以有效减少CPU的重复使用,节省服务器资源。
  • 静态内容也可以预先转换为Buffer的方式,使性能得到提升。
  • 对于大文件而言,highWaterMark的大小决定回触发系统调用和data事件的次数