阅读原文

Buffer 概述

在 ES6 引入 TypedArray 之前,JavaScript 语言没有读取或操作二进制数据流的机制。 Buffer 类被引入作为 NodeJS API 的一部分,使其可以在 TCP 流或文件系统操作等场景中处理二进制数据流。
Buffer 属于 Global 对象,使用时不需引入,且 Buffer 的大小在创建时确定,无法调整。

创建 Buffer

在 NodeJS v6.0.0 版本之前,Buffer 实例是通过 Buffer 构造函数创建的,即使用 new 关键字创建,它根据提供的参数返回不同的 Buffer,但在之后的版本中这种声明方式就被废弃了,替代 new 的创建方式主要有以下几种。

1、Buffer.alloc 和 Buffer.allocUnsafe

Buffer.allocBuffer.allocUnsafe 创建 Buffer 的传参方式相同,参数为创建 Buffer 的长度,数值类型。

// Buffer.alloc 和 Buffer.allocUnsafe 创建 Buffer
// Buffer.alloc 创建 Buffer
let buf1 = Buffer.alloc(6);// Buffer.allocUnsafe 创建 Buffer
let buf2 = Buffer.allocUnsafe(6);console.log(buf1); // <Buffer 00 00 00 00 00 00>
console.log(buf2); // <Buffer 00 e7 8f a0 00 00>

通过代码可以看出,用 Buffer.allocBuffer.allocUnsafe 创建 Buffer 是有区别的,Buffer.alloc 创建的 Buffer 是被初始化过的,即 Buffer 的每一项都用 00 填充,而 Buffer.allocUnsafe 创建的 Buffer 并没有经过初始化,在内存中只要有闲置的 Buffer 就直接 “抓过来” 使用。

Buffer.allocUnsafe 创建 Buffer 使得内存的分配非常快,但已分配的内存段可能包含潜在的敏感数据,有明显性能优势的同时又是不安全的,所以使用需格外 “小心”。

2、Buffer.from

Buffer.from 支持三种传参方式:

  • 第一个参数为字符串,第二个参数为字符编码,如 ASCIIUTF-8Base64 等等。
  • 传入一个数组,数组的每一项会以十六进制存储为 Buffer 的每一项。
  • 传入一个 Buffer,会将 Buffer 的每一项作为新返回 Buffer 的每一项。

传入字符串和字符编码:

// 传入字符串和字符编码
let buf = Buffer.from("hello", "utf8");console.log(buf); // <Buffer 68 65 6c 6c 6f>

传入数组:

// 数组成员为十进制数
let buf = Buffer.from([1, 2, 3]);console.log(buf); // <Buffer 01 02 03>
// 数组成员为十六进制数
let buf = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]);console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString("utf8")); // 你好

在 NodeJS 中不支持 GB2312 编码,默认支持 UTF-8,在 GB2312 中,一个汉字占两个字节,而在 UTF-8 中,一个汉字占三个字节,所以上面 “你好” 的 Buffer 为 6 个十六进制数组成。

// 数组成员为字符串类型的数字
let buf = Buffer.from(["1", "2", "3"]);console.log(buf); // <Buffer 01 02 03>

传入的数组成员可以是任何进制的数值,当成员为字符串的时候,如果值是数字会被自动识别成数值类型,如果值不是数字或成员为是其他非数值类型的数据,该成员会被初始化为 00

创建的 Buffer 可以通过 toString 方法直接指定编码进行转换,默认编码为 UTF-8

传入 Buffer:

// 传入一个 Buffer
let buf1 = Buffer.from("hello", "utf8");let buf2 = Buffer.from(buf1);console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // true
console.log(buf1[0] === buf2[0]); // false

当传入的参数为一个 Buffer 的时候,会创建一个新的 Buffer 并复制上面的每一个成员。

Buffer 为引用类型,一个 Buffer 复制了另一个 Buffer 的成员,当其中一个 Buffer 复制的成员有更改,另一个 Buffer 对应的成员会跟着改变,因为指向同一个引用,类似于 “二维数组”。

// Buffer 类比二维数组
let arr1 = [1, 2, [3]];
let arr2 = arr1.slice();arr2[2][0] = 5;
console.log(arr1); // [1, 2, [5]]

Buffer 的常用方法

1、fill

Buffer 的 fill 方法可以向一个 Buffer 中填充数据,支持传入三个参数:

  • value:将要填充的数据;
  • start:填充数据的开始位置,不指定默认为 0
  • end:填充数据的结束位置,不指定默认为 Buffer 的长度。
let buf = Buffer.alloc(3);buf.fill(1);
console.log(buf); // <Buffer 01 01 01>
let buf = Buffer.alloc(6);buf.fill(1, 2, 4);
console.log(buf); // <Buffer 00 00 01 01 00 00>

上面代码可以看出填充数据是 “包前不包后的”,fill 的第一个参数也支持是多个字节,从被填充 Buffer 的起始位置开始,一直到结束,会循环填充这些字节,剩余的位置不够填充这几个字节,会填到哪算哪,有可能不完整,如果 fill 指定的结束位置大于了 Buffer 的长度,会抛出 RangeError 的异常。

let buf = Buffer.alloc(6);buf.fill("abc", 1, 5);
console.log(buf); // <Buffer 00 61 62 63 61 00>
let buf = Buffer.alloc(3);buf.fill("abc", 4, 8);
console.log(buf); // throw new errors.RangeError('ERR_INDEX_OUT_OF_RANGE');

2、slice

Buffer 的 slice 方法与数组的 slice 方法用法完全相同,相信数组的 slice 已经足够熟悉了,这里就不多赘述了,Buffer 中截取出来的都是 Buffer。

let buf = Buffer.from("hello", "utf8");let a = buf.slice(0, 2);
let b = buf.slice(2);
let b = buf.slice(-2);console.log(a.toString()); // he
console.log(b.toString()); // llo
console.log(c.toString()); // o

3、indexOf

Buffer 的 indexOf 用法与数组和字符串的 indexOf 类似,第一个参数为查找的项,第二个参数为查找的起始位置,不同的是,对于 Buffer 而言,查找的可能是一个字符串,代表多个字节,查找的字节在 Buffer 中必须有连续相同的字节,返回连续的字节中第一个字节的索引,没查找到返回 -1

let buf = Buffer.from("你*好*吗", "utf8");console.log(buf); // <Buffer e4 bd a0 2a e5 a5 bd 2a e5 90 97>
console.log(buf.indexOf("*")); // 3
console.log(buf.indexOf("*", 4)); // 7

4、copy

Buffer 的 copy 方法用于将一个 Buffer 的字节复制到另一个 Buffer 中去,有四个参数:

  • target:目标 Buffer
  • targetStart:目标 Buffer 的起始位置
  • sourceStart:源 Buffer 的起始位置
  • sourceEnd:源 Buffer 的结束位置
// 容器 Buffer 长度充足
let targetBuf = Buffer.alloc(6);
let sourceBuf = Buffer.from("你好", "utf8");// 将 “你好” 复制到 targetBuf 中
sourceBuf.copy(targetBuf, 0, 0, 6);console.log(targetBuf.toString()); // 你好
// 容器 Buffer 长度不足
let targetBuf = Buffer.alloc(3);
let sourceBuf = Buffer.from("你好", "utf8");sourceBuf.copy(targetBuf, 0, 0, 6);
console.log(targetBuf.toString()); // 你

上面第二个案例中虽然要把整个源 Buffer 都复制进目标 Buffer 中,但是由于目标 Buffer 的长度只有 3,所以最终只能复制进去一个 “你” 字。

Buffer 与数组不同,不能通过操作 length 和索引改变 Buffer 的长度,Buffer 一旦被创建,长度将保持不变。

// 数组对比 Buffer —— 操作 length
// 数组
let arr = [1, 2, 3];
arr[3] = 4;
console.log(arr); // [1, 2, 3, 4]arr.length = 5;
console.log(arr); // [1, 2, 3, 4, empty]// Buffer
let buf = Buffer.alloc(3);
buf[3] = 0x00;
console.log(buf); // <Buffer 00 00 00>buf.length = 5;
console.log(buf); // <Buffer 00 00 00>
console.log(buf.length); // 3

通过上面代码可以看出数组可以通过 length 和索引对数组的长度进行改变,但是 Buffer 中类似的操作都是不生效的。

copy 方法的 Polyfill:

// 模拟 copy 方法
Buffer.prototype.myCopy = function (target, targetStart, sourceStart, sourceEnd) {for(let i = 0; i < sourceEnd - sourceStart; i++) {target[targetStart + i] = this[sourceStart + i];}
}

5、Buffer.concat

与数组类似,Buffer 也存在用于拼接多个 Buffer 的方法 concat,不同的是 Buffer 中的 concat 不是实例方法,而是静态方法,通过 Buffer.concat 调用,且传入的参数不同。

Buffer.concat 有两个参数,返回值是一个新的 Buffer:

  • 第一个参数为一个数组,数组中的每一个成员都是一个 Buffer;
  • 第二个参数代表新 Buffer 的长度,默认值为数组中每个 Buffer 长度的总和。

Buffer.concat 会将数组中的 Buffer 进行拼接,存入新 Buffer 并返回,如果传入第二个参数规定了返回 Buffer 的长度,那么返回值存储拼接后前规定长度个字节。

let buf1 = Buffer.from("你", "utf8");
let buf2 = Buffer.from("好", "utf8");let result1 = Buffer.concat([buf1, buf2]);
let result2 = Buffer.concat([buf1, buf2], 3);console.log(result1); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result1.toString()); // 你好console.log(result2); // <Buffer e4 bd a0>
console.log(result2.toString()); // 你

Buffer.concat 方法的 Polyfill:

// 模拟 Buffer.concat
Buffer.myConcat = function (bufferList, len) {// 新 Buffer 的长度len = len || bufferList.reduce((prev, next) => prev + next.length, 0);let newBuf = Buffer.alloc(len); // 创建新 Bufferlet index = 0; // 下次开始的索引// 循环存储 Buffer 的数组进行复制bufferList.forEach(buf => {buf.myCopy(newBuf, index, 0, buf.length);index += buf.length;});return newBuf;
}

6、Buffer.isBuffer

Buffer.isBuffer 是用来判断一个对象是否是一个 Buffer,返回布尔值。

let obj = {};
let buf = Buffer.alloc(6);console.log(Buffer.isBuffer(obj)); // false
console.log(Buffer.isBuffer(buf)); // true

封装一个 split

字符串中的 split 是经常使用的方法,可以用分隔符将字符串切成几部分存储在数组中,Buffer 本身没有 split 方法,但是也会有类似的使用场景,所以我们在 Buffer 中自己封装一个 split

Buffer 的 split 方法参数为一个分隔符,这个分隔符可能是一个或多个字节的内容,返回值为一个数组,分隔开的部分作为独立的 Buffer 存储在返回的数组中。

// 封装 Buffer 的 split 方法
Buffer.prototype.split = function (sep) {let len = Buffer.from(sep).length; // 分隔符所占的字节数let result = []; // 返回的数组let start = 0; // 查找 Buffer 的起始位置let offset = 0; // 偏移量// 循环查找分隔符while ((offset = this.indexOf(sep, start)) !== -1) {// 将分隔符之前的部分截取出来存入result.push(this.slice(start, offset));start = offset + len;}// 处理剩下的部分result.push(this.slice(start));// 返回结果return result;
}

验证 split 方法:

// 验证 split
let buf = Buffer.from("哈登爱篮球爱夜店", "utf8");
let bufs = buf.split("爱");console.log(bufs);
// [ <Buffer e5 93 88 e7 99 bb>,
//   <Buffer e7 af ae e7 90 83>,
//   <Buffer e5 a4 9c e5 ba 97> ]newBufs = bufs.map(buf => buf.toString());
console.log(newBufs); // [ '哈登', '篮球', '夜店' ]

Buffer 的编码转换

我们知道 NodeJS 中的默认编码为 UTF-8,且不支持 GB2312 编码,假如现在有一个编码格式为 GB2312txt 文件,内容为 “你好”,现在我们使用 NodeJS 去读取它,由于在 UTF-8GB2312 编码中汉字所占字节数不同,所以读出的内容无法解析,即为乱码。

// 引入依赖
const fs = require("fs");
const path = require("path");let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString()); // 你好
console.log(result); // <Buffer c4 e3 ba c3>
console.log(result.toString()); // ���

如果一定要在 NodeJS 中来正确解析这样的内容,这样的问题还是有办法解决的,我们需要借助 iconv-lite 模块,这个模块可以将一个 Buffer 按照指定的编码格式进行编码或解码。

由于 iconv-lite 是第三方提供的模块,在使用前需要安装,安装命令如下:

npm install iconv-lite

如果想正确的读出其他编码格式文件的内容,上面代码应该更改为:

// 引入依赖
const fs = require("fs");
const path = require("path");
const iconvLite = require("iconv-lite");let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));console.log(iconvLite.decode(result, "gb2312")); // 你好

去掉 BOM 头

上面读取 GB2312 编码的 txt 文件也可以通过打开文件重新保存为 UTF-8 或用编辑器直接将编码手动修改为 UTF-8,此时读取的文件不需要进行编码转换,但是会产生新的问题。

// 产生 BOM 头
// 引入依赖
const fs = require("fs");
const path = require("path");let buf = Buffer.from("你好", "utf8");
let result = fs.readFileSync(path.resolve(__dirname, "a.txt"));console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(result); // <Buffer ef bb bf e4 bd a0 e5 a5 bd>

在手动修改 txt 文件编码后执行上面代码,发现读取的 Buffer 与正常情况相比前面多出了三个字节,只要存在文件编码的修改就会在这个文件的前面产生多余的字节,叫做 BOM 头。

BOM 头是用来判断文本文件是哪一种 Unicode 编码的标记,其本身是一个 Unicode 字符,位于文本文件头部。

虽然 BOM 头起到了标记文件编码的作用,但是它并不属于文件的内容部分,因此会产生一些问题,如文件编码发生变化后无法正确读取文件的内容,或者多个文件在合并的过程中,中间会夹杂着这些多余内容,所以在 NodeJS 文件操作的源码中,Buffer 编码转换的模块 iconv-lite 中,以及 Webpack 对项目文件进行打包编译时都进行了去掉 BOM 头的操作。

为了让上面的代码可以正确的读取并解析编码被手动修改过的文件内容,我们这里也需要进行去掉 BOM 头的操作。

// 去掉 BOM 头的方法
function BOMStrip(result) {if (Buffer.isBuffer(result)) {// 如果读取的内容为 Bufferif (result[0] === 0xef && result[1] === 0xbb && result[2] === 0xbf) {// 若前三个字节是否和 BOM 头的前三字节相同,去掉 BOM 头return Buffer.slice(3);}} else {// 如果不是 Bufferif (result.charCodeAt(0) === 0xfeff) {// 判断第一项是否和 BOM 头的十六进制相同,去掉 BOM 头return result.slice(1);}}
}

使用去掉 BOM 头的方法并验证上面读文件的案例:

// 验证去 BOM 头的方法
// 引入依赖
const fs = require("fs");
const path = require("path");// 两种方式读文件
let result1 = fs.readFileSync(path.resolve(__dirname, "a.txt"));
let result2 = fs.readFileSync(path.resolve(__dirname, "a.txt"), "utf8");console.log(BOMStrip(result1).toString()); // 你好
console.log(BOMStrip(result2)); // 你好

缓存 Buffer

// 产生乱码问题
let buf = Buffer.from("你好", "utf8");let a = buf.slice(0, 2);
let b = buf.slice(2, 6);console.log(a.toString()); // �
console.log(b.toString()); // �好

UTF-8 编码,一个汉字三个字节,使用 slice 方法对一个表达汉字的 Buffer 进行截取,如果截取长度不是 3 的整数倍,此时无法正确解析,会显示乱码,类似这种情况可以使用模块 string_decoder 对不能组成汉字的 Buffer 进行缓存,string_decoder 是核心模块,不需要安装。

// 缓存 Buffer
// 引入依赖
const { StringDecoder } = require("string_decoder");let buf = Buffer.from("你好", "utf8");let a = buf.slice(0, 2);
let b = buf.slice(2, 6);// 创建 StringDecoder 实例
let sd = new StringDecoder();console.log(sd.write(a));
console.log(sd.write(b)); // 你好

上面代码中使用了 string_decoder 后,截取的 Buffer 不能组成一个汉字的时候不打印,进行缓存,等到可以正确解析时取出缓存,重新拼接后打印。

NodeJS —— Buffer 解读相关推荐

  1. Nodejs: Buffer报错argument must be an Array of Buffer or Uint8Array instances

    最近项目有个需求需要在和第三方API接口进行交互时,在JSON字符串最前面加上16个字节的签名字节,为了简化问题忽略JSON以及16个字节的具体取值. 错误代码: buff = Buffer.from ...

  2. NodeJS Buffer(缓冲区)

    2019独角兽企业重金招聘Python工程师标准>>> JavaScript 语言自身只有字符串数据类型,没有二进制数据类型. 但在处理像TCP流或文件流时,必须使用到二进制数据.因 ...

  3. NodeJS 文件操作 —— fs 基本使用

    阅读原文 fs 概述 在 NodeJS 中,所有与文件操作都是通过 fs 核心模块来实现的,包括文件目录的创建.删除.查询以及文件的读取和写入,在 fs 模块中,所有的方法都分为同步和异步两种实现,具 ...

  4. Linux buffer/cache解读

     cache与buffer解读 cache出现的原因与功能 计算机硬件中CPU.内存.磁盘是最主要的三大部分,其中,CPU发展到今天,执行速度最快,而内存相对CPU而言,就慢多了,CPU执行的指令是从 ...

  5. 知秋源码解读分享系列

    作为一个乐于分享的人,我希望通过一些成熟优秀的代码库,来向大家展示读源码思路以及阐述编程方面的技巧,也希望大家从中思考并得到属于自己的一套编程方法论. 半年以来,已进行72小时时长的源码解读分享视频录 ...

  6. NodeJs 面试题 2023

    (要知道对好事的称颂过于夸大,也会招来人们的反感轻蔑和嫉妒.--培根) ㅤㅤㅤ ㅤㅤㅤ ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ NodeJs相关 什么是NodeJs Nodejs是一个基于V8虚拟机的Jav ...

  7. Android Systrace 基础知识(10) - Binder 和锁竞争解读

    本文是 Systrace 系列文章的第十篇,主要是对 Systrace 中的 Binder 和锁信息进行简单介绍,简单介绍了 Binder 的情况,介绍了 Systrace 中 Binder 通信的表 ...

  8. 知秋源码解读分享系列(转)

    前序 知秋:人称飞哥,Java 界真·大佬.我线上认识的朋友,线下也见过好多次.认识这么多年以来给我的印象就是:人好技术更好,在 Java 响应式编程这个领域有着自己独到的见解,并且一直以来都默默无闻 ...

  9. Android Systrace 基础知识(9)-MainThread 和 RenderThread 解读

    本文是 Systrace 系列文章的第九篇,主要是是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的「主线程」和「渲染线程」.文章会从 Syst ...

最新文章

  1. PHP学习笔记 第八讲 Mysql.简介和创建新的数据库
  2. Spring和MyBatis环境整合
  3. 通过条件判断文本框是否隐藏_如何通过风速来判断高效过滤器是否达到更换要求...
  4. flutter: 根视图、根元素与根渲染
  5. 强制推送代码到远程仓库
  6. 【python】将excel转成json
  7. CImage 对话框初始化时候显示透明 PNG
  8. 软件测试--计算机基础
  9. 分享功能 集成友盟分享
  10. duilib开发(十二):使用自定义控件
  11. ViewPage实现一屏多页面显示(进阶版)
  12. :幽灵蛛(pholcus)(三)--header get post学习资料
  13. glassfish插件_安装和使用Glassfish
  14. xp重启计算机的快捷键,xp电脑关机重启快捷键如何使用
  15. treeset可以重复吗_社保和商业医疗险可以重复报销吗?报销攻略请收好
  16. 什么是增值税的进项税和销项税?
  17. mysql 简述pk uk fk 的区别和对数据库性能的影响_数据库pk fk ak
  18. 历经四个月,谷歌联盟的PIN码问题终于解决了
  19. C语言实现两个多项式加法与乘法
  20. P4设计实现链路监控

热门文章

  1. 如何将页面m3u8、blob类型的视频链接下载下来?
  2. Lesson6 【LINUX】Linux系统快速上手
  3. 音乐下载器源码,需要自取
  4. 判断2000-2500年中的每一年是否为闰年,将结果输出。
  5. 介绍Jackson JsonNode和ObjectNode
  6. Python程序设计(一 认识Python)—— 认识Python
  7. 抗生素耐药性革兰氏阴性菌感染的治疗,2020最新IDSA指南
  8. storcli64和smartctl定位硬盘的故障信息
  9. containerd 配置镜像仓库完全攻略
  10. Spring AOP(二)@within() 和 @target