# 深入浅出Buffer

# 编码的发展

常见的编码有以下几种

  • ASCll编码
  • GB2312
  • GBK
  • GB18030/DBCS
  • Unicode
  • Utf8

其实它们之间是有关系的,拿GB系列来说,如果1个字节,那还是表示ASCLL码(127),但是如果是2个字节,就表示汉字。 UTF8也一样,不同的是UTF8是3个字节表示一个汉字。

但是在我们node中,是不支持GBK编码的,我们可以尝试下读取GDK编码的文件:




 

// 1.txt 陈颖 GBK
const fs = require('fs')
const res = fs.readFileSync('./1.txt')
console.log(res.toString()) //��ӱ

我们可以发现在读取GBK格式的文件的时候,会出现乱码,这也是因为node不支持GBK编码,但是我们有时候再用node爬虫的时候难免会遇到目标是GBK编码的,那我们可以使用一个第三方iconv-lite来对GBK编码的内容进行转码,一般转换为UTF8




 



// 1.txt 陈颖 GBK
const fs = require('fs')
const iconv = require('iconv-lite')
const res = fs.readFileSync('./1.txt')
const result = iconv.decode(res, 'gbk')
console.log(result.toString()) // 陈颖

so easy too happy

# Buffer的常见用法和原理

因为在node中需要处理网络协议、操作数据库、处理图片、接受上传文件,因此,需要大量操作二进制数据,所以Buffer就诞生了。Bufferglobal上的属性,他表示的是内存,他将二进制转化为了十六进制存起来,来表示二进制,可以和字符串之间通过toString() 进行转换。

如二进制的 11111111 表示 255,在Buffer中用十六进制表示,也就是 0Xff

Buffer的基本概念有个了解之后,我们看一下它的一些常见用法

# 声明 Buffer - Buffer.fron() or Buffer.alloc

先来看下基本用法




 

let buffer1 = Buffer.alloc(3)
let buffer2 = Buffer.from('陈颖')
console.log(buffer1) //<Buffer 00 00 00>
console.log(buffer2) //<Buffer e9 99 88 e9 a2 96>

我们可以发现,使用Buffer.alloc(num)是声明一个长度为numBuffer内存,而Buffer.from(str) 是根据 str的内容大小自动生成合适大小的 Buffer内存。

TIP

注:Buffer内存一旦申请好,就不能更改了

# 拷贝 Buffer - buffer.copy()

在开发的时候我们经常会遇到这样一个需求,将2个Buffer的内存区块合并到一个大的Buffer当中去,这个时候我们可以用到buffer.copy(),将小的Buffer分别拷贝到大的Buffer中去




 




let buffer1 = Buffer.from('全栈')
let buffer2 = Buffer.from('陈颖')
let bigBuffer = Buffer.alloc(12) // 因为一个汉字占3个字节,这里4个汉字需要12字节
buffer1.copy(bigBuffer, 0, 0, 6)
buffer2.copy(bigBuffer, 6, 0, 6)
console.log(bigBuffer) // <Buffer e9 99 88 e9 a2 96 00 00 00 00 00 00>
console.log(bigBuffer.toString()) // 全栈陈颖

buffer.copy(targetBuffer, targetStart, sourceStart, sourceEnd)中的参数意义如下

targetBuffer -- 目标Buffer
targetStart -- 目标Buffer的开始索引
sourceStart -- 源Buffer的开始索引
sourceEnd -- 源Buffer的结束索引

既然用法和参数我们都搞懂了,我们试一下自己实现一个这个方法




 









Buffer.prototype.myCopy = function (targetBuffer, targetStart, sourceStart, sourceEnd) {
    for (let i = 0; i< sourceEnd - sourceStart ; i++) {
        targetBuffer[targetStart + i] = this[i]
    }
}
let buffer1 = Buffer.from('全栈')
let buffer2 = Buffer.from('陈颖')
let bigBuffer = Buffer.alloc(12) // 因为一个汉字占3个字节,这里4个汉字需要12字节
buffer1.myCopy(bigBuffer, 0, 0, 6)
buffer2.myCopy(bigBuffer, 6, 0, 6)
console.log(bigBuffer) // <Buffer e9 99 88 e9 a2 96 00 00 00 00 00 00>
console.log(bigBuffer.toString()) // 全栈陈颖

其实内部原理很简单,将源Buffer通过循环一个一个提取出来往目标Buffer里面塞

# 合并 Buffer - Buffer.concat()

还是这个需求,将几个小Buffer合并成一个大Buffer,如果用buffer.copy() 的话难免有点麻烦,node也为我们提供了一个便捷的方法 Buffer.concat(targetBuffers, lenght)




 



let buffer1 = Buffer.from('全栈')
let buffer2 = Buffer.from('陈颖')
const newBuffer = Buffer.concat([buffer1, buffer2], 9)
console.log(newBuffer) // <Buffer e5 85 a8 e6 a0 88 e9 99 88>
console.log(newBuffer.toString()) // 全栈陈 
// 因为 concat 的第二个参数传了 9 ,也就是只取3个汉字返回

Buffer.concat(targetBuffers, lenght)中的参数意义如下

targetBuffers -- 将要合并的源Buffer数组
lenght -- 最终得到的Buffer的长度(根据这个长度进行截取)

既然用法和参数我们都搞懂了,我们试一下自己实现一个这个方法




 







Buffer.myConcat = function (targetBuffers, lenght = targetBuffers.reduce((a, b) => a + b.length, 0)) {
    // lenght = targetBuffers.reduce((a, b) => a + b.length, 0) 的意思是将targetBuffers遍历,得到返回Buffer的长度
    let buffer = Buffer.alloc(lenght) // 根据长度申请Buffer
    let offset = 0 // 返回Buffer的偏移量,因为循环中用到了 buffer.copy() 方法,所以需要记录偏移量的值
    targetBuffers.forEach(item => {
        item.copy(buffer, offset, 0, item.length)
        offset += item.length // 更新偏移量
    })
    return buffer
}

其实 Buffer.concat() 就是基于 buffer.copy() 更深一层的封装

Buffer和数组很像,还有一些常用的方法我们就简单的列举下就好了




 

buffer.slice(start, end) --将buffer进行截取,返回一个新的Buffer
Buffer.isBuffer(target)  --判断目标是否是一个Buffer,返回一个波尔,这个方法在koa源码中有用到,判断是否是一个Buffer,来对应的进行返回
buffer.indexOf(str)  --判断当前str在Buffer中的索引 这里需要注意一个汉字是3个字节
---

# 实现 buffer.split()

有时候我们需要Buffer和数组一样,也拥有 split()切割数组的方法,但是node里面没有给我们提供一个这样的方法,那我们就去简单的实现一个这个方法




 




















let buffer = Buffer.from('匀全栈匀陈匀颖爱匀前端')

Buffer.prototype.split = function (sep) {
    let arr = []
    let offset = 0 // 偏移量
    let current // 当前索引
    let len = Buffer.from(sep).length // 获取分割符的长度
    while (-1 != (current = this.indexOf(sep, offset))) {
        // 如果找到,就一直循环
        arr.push(this.slice(offset, current)) // 将找到的buffer放入数组
        offset = current + len // 偏移量 = 当前索引 + 分隔符的长度
    }
    arr.push(this.slice(offset)) // 别忘了最末尾的一个buffer

    return arr
}

let res = buffer.split('匀').map(item => {
  return item.toString()
})
console.log(res) //[ '', '全栈', '陈', '颖爱', '前端' ]

# base64

之前很久以前一直没有理解这个 base64,还以为它是一个加密算法,emmmmmmmmmmmm,其实 base是一种编码

我们知道一个汉字3个字节,一个字节8位,也就是 3 * 8,而base64 就是将这种 3 * 8 的结构转为 4 * 6的结构,即一个汉字4个字节,一个字节6位,斋说有点干,来个例子就完事了





 


























// 我们知道 Buffer是16进制的
let buffer = Buffer.from('颖')
console.log(buffer) //<Buffer e9 a2 96>

// 将16进制的结果转为2进制
console.log(0xe9.toString(2)) // 11101001
console.log(0xa2.toString(2)) // 10100010
console.log(0x96.toString(2)) // 10010110

// 11101001 10100010 10010110 这就是我们说的  3 * 8的结构
// 那现在我们对结构进行转换下,换成 4 * 6的结构,得到的结果如下
// 111010 011010 001010 010110 
// 但是一个字节为8位,这里只有6位,那我们对它们进行补0处理,得到的结构如下
// 00111010 00011010 00001010 00010110 到这一步我们就发现了,base64比 3 * 8 的结构大三分之一

// 将2进制转为10进制
console.log(parseInt('00111010', 2)) //58
console.log(parseInt('00011010', 2)) //26
console.log(parseInt('00001010', 2)) //10
console.log(parseInt('00010110', 2)) //22

// 58 26 10 22 这里拿到的十进制数字,其实对应着一个字符串中某个字符的索引,我们先把实现这个字符串
let str = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`
str += str.toLowerCase()
str += `0123456789+/`
console.log(str) // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
console.log(str[58] + str[26] + str[10] + str[22]) // 6aKW  得到的就是 base64的编码了

// 上述只是为了让我们对base64的转化流程有了一个清晰的认识,但是在实际中我们只需要一个api就搞定了
console.log(buffer.toString('base64')) // 6aKW