buffer的使用

223 阅读6分钟

本章 api 较多,为了更友好的代码提示,建议安装 @types/node 插件。

npm i @types/node -g

Buffer 是什么

缓冲区,它用来表示固定长度的 16 进制地址值的序列,JS 底层存储的都是二进制数据,二进制中一个字节,需要 8 个位,最大是 11111111,而 16 进制表示一个字节, 则只需要 2 位, 最大是 ff,表示起来简短许多。

// 二进制的 11111111 转十进制 => 255
// 十进制的 255 转 16 进制 => ff, 所以 16 进制表示字节,只需要 2 位即可。
// 这也是 buffer 的结果都是 2 位的原因,比如 <Buffer 0e cd 00>
255..toString(16); // ff

Buffer 表示的是固定长度的序列,就是说,一旦声明好,就不能随意扩容,如果想去修改 buffer 大小,改小可以截取内存,改大的话需要创造一个大的内存空间,将数据拷贝过去。

node 中如果使用二进制来表示的话,因为 node 中文都是 utf8 编码,一个汉字占用三个字节,每个字节又有8个位,那么表示一个汉字就需要 24 个二进制位,这样从文件中读取出来的数据就会异常的长。

需要注意的是,node 中单个字节就能表示的(英文,数字,符号等)使用的是 ascii 编码,需要多个字节的使用的是 uft8 编码,node 中所谓的数据转 16 进制,实际上转的只是对应编码映射表上的 key (10进制),具体还要转 10 进制,再去对应映射表上查找。

而浏览器中直接展示的就是十进制的数字,而不需要去 ascii 表上查找,因为浏览器最初是不能读写文件的,所以浏览器底层实现就规定了页面上展示的数字就是十进制的,而文字则是 utf8 编码 (charset=utf-8)

注意,编码和进制不是一个东西,编码可以理解成映射表,页面上显示给用户看到的是映射后的值,而进制数是用来描述这个数据在编码映射表中的位置的,所以进制数其实都是数字。

var bf = Buffer.from('1'); // 将 1 转 1 6进制

console.log(bf); // <Buffer 31>

// 16 进制 31 转 10 进制
3 * 16 + 1 * 1 = 49;

// 还需要去 ascii 映射表上查找 49,才能得到 1 

Buffer 常用方法

new Buffer 创建 buffer(已弃用)

老的用法 (因为一些安全性问题,已经被弃用):

let bf = new Buffer('帅');

// <Buffer e5 b8 85>  
// DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.

Buffer.alloc 接收 size, 创建 buffer

// alloc 传入数字,代表创建几个字节大小的 Buffer 空间,十六进制值默认填最小值 00
let bf = Buffer.alloc(3);  // <Buffer 00 00 00>
let bf2 =Buffer.alloc(1024);  // 1024个字节  1kb

Buffer.from 接收数组,创建 buffer

接收一个由 16 进制地址组成的数组。

// 很少使用数组来定义 buffer,因为需要指定存放的内容
Buffer.from([0xff, 0xff]);  // <Buffer ff ff>

// 我们知道,二进制一个字节最大数组是八个 1,也就代表 255,转成 16 进制为 ff
// 255..toString(16)
// 也就是说,16 进制一个字节表示最大数为 255
// 超过 255 则取余 (0 - 255 共 256 位, 对 256 取余所以 256 代表 1)
Buffer.from([257, 0xff,0xff]); // <Buffer 01 ff ff>
Buffer.from([-1, 0xff,0xff]); // <Buffer ff ff ff>

// 数组内必须是 16 进制的值(0x开头,0-9,a-f),或者 10 进制的数字
Buffer.from(['a', 0xff,0xff]); // <Buffer 00 ff ff> "a" 没有解析出来
Buffer.from([2, 0xff,0xff]); // <Buffer 00 ff ff>

Buffer.from 接收字符串,创建 buffer(主要用法)

Buffer 的主要用法,返回一个传入字符串转成的 16进制值序列的缓冲区

Buffer.from('杨帅'); // <Buffer e6 9d a8 e5 b8 85> 两个汉字 6个字节

Buffer.isBuffer

判断是不是一个 Buffer

let bf = Buffer.from('杨帅');

console.log(Buffer.isBuffer(bf)); // true
console.log(Buffer.isBuffer('a')); // falses

Buffer.toString

16 进制 buffer 转字符串,默认 utf8 编码,一般我们常用的就是 utf8 和 base64,但是不支持 gbk 哦。

let bf = Buffer.from('杨帅');

bf.toString(); // 杨帅
bf.toString('base64'); // 5p2o5biF

Buffer 下标访问

浏览器的特性,展示出来的都是 10 进制,所以下标访问会转 10 进制。

let bf = Buffer.from('杨帅');

console.log(bf); // <Buffer e6 9d a8 e5 b8 85>
console.log(bf[bf.length - 1]); // 133  打印出来会转 10 进制 8 * 16**1 + 5 = 133

Buffer.length

需要特别注意的一点是,Buffer 的长度指的是字节长度,不是字符串的长度哦

const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');

console.log(bf1.length, bf2.length); // 6  6

// 想拿到源字符串的长度需要 toString
console.log(bf1.toString().length }); // 2

Buffer.slice

类似数组截取,不同的是,buffer 中存的是内存地址,所有截取后修改,二者同时改变。

const bf = Buffer.from([1, 2, 3, 4]);
const bf2 = bf.slice(0, 1);

console.log(bf2); // <Buffer 01>

bf2[0] = 100; 
console.log(bf); // <Buffer 64 02 03 04>

Buffer.copy

const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');

console.log(bf1.length, bf2.length);

const bigBf = Buffer.alloc(12); // 12 个字节的 buff

// 把 buf1 拷贝到 bigBf 上
// 要拷贝到的目标 buffer,目标 buffer 的开始,buf1 的开始拷贝的下标,结束拷贝的下标
bf1.copy(bigBf, 0, 0, 6);

bf2.copy(bigBf, 6, 0, 3);

console.log(bigBf.toString()); // 哈哈哎

Buffer.concat 合并

const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');

// 这个 9 代表拼接后的结果只有 9 个字节,也就是只拼三个字
console.log(Buffer.concat([bf1, bf2], 9).toString()); // 哈哈哎

Buffer 的应用场景(文件操作)

// note.md
123456789
// buffer.js
const fs = require('fs');
const path = require('path');

// 不给编码,默认读取出来的都是 buffer(16进制)
let r = fs.readFileSync(path.resolve(__dirname, './note.md'));

console.log(r); // <Buffer 31 32 33 34 35 36 37 38 39>

有同学可能会疑问,为什么我的 10 进制的 1 读取出来是 16 进制的 31,16 进制的 31 转 10 进制却是 49 呢

3 * 16**1 + 1 * 16**0 = 49

这是因为 node 中数字的编码方式是 ascii,ascii 表上 49 表示的就是 1

浏览器的特性,展示出来的都是 10 进制,所以下标访问会转 10 进制:

console.log(r[0]); // 49

手动实现 copy 方法

/**
 * @description 手动实现 Buffer copy 方法
 * @param {*} target 要拷贝到的目标 buffer
 * @param {*} targetStart 目标 buffer 写入下标
 * @param {*} sourceStart 源 buffer 的开始下标 不写默认 0
 * @param {*} sourceEnd 源 buffer 的结束下标 不写默认到最后
 */
 Buffer.prototype.copy = function(target, targetStart, sourceStart = 0, sourceEnd = this.length) {
  for (let i = sourceStart; i < sourceEnd; i++) {
    target[targetStart++] = this[i];
  }
}

const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');
const bigBf = Buffer.alloc(12); // 12 个字节的 buff

bf1.copy(bigBf, 0, 0, 6);
bf2.copy(bigBf, 6, 3, 6);

console.log(bigBf.toString());

手动实现 concat 方法

/**
 * @description 手动实现 Buffer concat 静态方法
 * @param {*} bufferList 
 * @param {*} len 
 * @returns 
 */
Buffer.concat = function(bufferList, len) {
  if (len == undefined) {
    // 如果没传 len 默认全部合并
    len = bufferList.reduce((a, b) => a + b.length, 0);
  }

  let bigBuffer = Buffer.alloc(len);
  let offset = 0;

  for (let i = 0; i < bufferList.length; i++) {
    let buf =  bufferList[i];

    // 如果当前项是 buffer,再合并进去
    if (Buffer.isBuffer(buf)) {
      buf.copy(bigBuffer, offset);
      offset += buf.length;
    }
  }

  return bigBuffer;
}

const bf1 = Buffer.from('哈哈');
const bf2 = Buffer.from('哎嘿');

console.log(Buffer.concat(['a', bf1, bf2], 9).toString()); // 哈哈哎嘿