# 深入浅出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
就诞生了。Buffer
是 global
上的属性,他表示的是内存,他将二进制转化为了十六进制存起来,来表示二进制,可以和字符串之间通过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)
是声明一个长度为num
的 Buffer
内存,而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