Node.js之Buffer(缓冲区)

一、Buffer初识

Buffer 类是作为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。这是来自 Node.js 官网的一段描述,比较晦涩难懂,总结起来一句话 Node.js 可以用来处理二进制流数据或者与之进行交互

Buffer 用于读取或操作二进制数据流,做为 Node.js API 的一部分使用时无需 require,用于操作网络协议、数据库、图片和文件 I/O 等一些需要大量二进制数据的场景。Buffer 在创建时大小已经被确定且是无法调整的,在内存分配这块 Buffer 是由 C++ 层面提供而不是 V8 具体后面会讲解。

在这里不知道你是否认为这是很简单的?但是上面提到的一些关键词二进制流(Stream)缓冲区(Buffer),这些又都是什么呢?下面尝试做一些简单的介绍。

什么是二进制数据?

谈到二进制我们大脑可能会浮想到就是 010101 这种代码命令,如下图所示:

正如上图所示,二进制数据使用 0 和 1 两个数码来表示的数据,为了存储或展示一些数据,计算机需要先将这些数据转换为二进制来表示。例如,我想存储 66 这个数字,计算机会先将数字 66 转化为二进制 01000010 表示,印象中第一次接触这个是在大学期间 C 语言课程中,转换公式如下所示:

上面用数字举了一个示例,我们知道数字只是数据类型之一,其它的还有字符串、图像、文件等。例如我们对一个英文 M 操作,在 JavaScript 里通过 'M'.charCodeAt() 取到对应的 ASCII 码之后(通过以上的步骤)会转为二进制表示。

什么是 Stream?

流,英文 Stream 是对输入输出设备的抽象,这里的设备可以是文件、网络、内存等。

流是有方向性的,当程序从某个数据源读入数据,会开启一个输入流,这里的数据源可以是文件或者网络等,例如我们从 a.txt 文件读入数据。相反的当我们的程序需要写出数据到指定数据源(文件、网络等)时,则开启一个输出流。当有一些大文件操作时,我们就需要 Stream 像管道一样,一点一点的将数据流出。

🌰举个例子

我们现在有一大罐水需要浇一片菜地,如果我们将水罐的水一下全部倒入菜地,首先得需要有多么大的力气(这里的力气好比计算机中的硬件性能)才可搬得动。如果,我们拿来了水管将水一点一点流入我们的菜地,这个时候不要这么大力气就可完成。

 

什么是 Buffer?

通过以上 Stream 的讲解,我们已经看到数据是从一端流向另一端,那么他们是如何流动的呢?

通常,数据的移动是为了处理或者读取它,并根据它进行决策。伴随着时间的推移,每一个过程都会有一个最小或最大数据量。如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理。

这里的等待区就指的缓冲区(Buffer),它是计算机中的一个小物理单位,通常位于计算机的 RAM 中。这些概念可能会很难理解,不要担心下面通过一个例子进一步说明。

🌰举个例子

举一个公共汽车站乘车的例子,通常公共汽车会每隔几十分钟一趟,在这个时间到达之前就算乘客已经满了,车辆也不会提前发车,早到的乘客就需要先在车站进行等待。假设到达的乘客过多,后到的一部分则需要在公共汽车站等待下一趟车驶来。

在上面例子中的等待区公共汽车站,对应到我们的 Node.js 中也就是缓冲区(Buffer),另外乘客到达的速度是我们不能控制的,我们能控制的也只有何时发车,对应到我们的程序中就是我们无法控制数据流到达的时间,可以做的是能决定何时发送数据。

 二、Buffer的结构

上面解析了什么是Buffer和与其相关的一些概念,为了更深入的了解nodejs中的Buffer,还需要了解Buffer这个模块在nodejs的结构。

1. Buffer模块结构

前面说过Buffer是全局作用域上的一个模块,可以理解为它是全局上的一个属性,这个属性引用着Bufeer模块对象,从这个角度来说它是一个JavaScript模块。但是JavaScript自身不具备直接操作二进制文件的能力,所以实质上Buffer在nodejs底层上还有一个C++模块。这是因为IO操作是非常消耗性能的,所以nodejs在Buffer模块构建设计上,将性能部分用C++实现,将非性能部分用JavaScript实现。如图所示:

 2. Buffer对象结构

Buffer在nodejs的javaScript中是一个对象,与数组非常类似,它的元素为16进制的两位数,即0到255的数值。

let buffer = Buffer.from('nodejs的学习路径')
console.log(buffer); //<Buffer 6e 6f 64 65 6a 73 e7 9a 84 e5 ad a6 e4 b9 a0>

不同编码的字符占用的元素各不相同,utf-8编码汉字占用3个元素,字母和半角标点符号占用1个元素,所以上面的str转成Buffer由21个元素组成。跟数组一样,Buffer可以使用length属性获取长度,也可以通过下标访问元素,构造Buffer对象时也可以和数组一样直接设置长度。

let buffer = Buffer.from('nodejs的学习路径')
console.log(buffer.length);    //21
console.log(buffer[3]);        //101
console.log(Buffer.alloc(30));//<Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>

了解了Buffer对象的基本结构,接着就来看看它的元素取值策略,前面说过它的元素为16进制,那它在不赋值的情况下会如何取值?如果超出16进制取值范围会发生什么?先来看下面的测试代码:

let buffer = Buffer.alloc(10)

console.log(buffer[10]);//undefined
buffer[0] = -10;
buffer[1] = -260;
buffer[2] = 260;
buffer[3] = 515;
console.log(buffer);   //<Buffer f6 fc 04 03 00 00 00 00 00 00>

当使用new Buffer()创建一个新的空Buffer对象时,它的元素值nodejs8版本前是0~255之间的随机值(为什么是随机值后面会解释),之后时0。然后是关于Buffer对象元素的赋值,前面说过Buffer对象元素是16进制的两位数,即0~255。

🔊:

赋值小于0时,就将该值逐次加256直至这个元素值大于或等于0。所以buf[0]赋值为-10实际上的取值是-10+256=246,即f6;赋值为-260的buf[1]实际取值是-260+256+256=252。
赋值大与256时,就将该值逐次减去256直至这个元素值小于或等于256。所以buf[2]赋值为260实际取值是260-256=4,即04;赋值为515的buf[3]实际取值是515-256-256=3,即03。

二、Buffer基本使用

创建Buffer

🔊 在 6.0.0 之前,Node.js 版本中, Buffer 实例是使用 Buffer 构造函数创建的,该函数根据提供的参数以不同方式分配返回的 Buffer

new Buffer()。 //🙅:不再推荐

🔊在 6.0.0 之后,现在可以通过 Buffer.from()、Buffer.alloc() 与 Buffer.allocUnsafe() 三种方式来创建

⏰ Buffer.from()

const b1 = Buffer.from('10'); //返回一个被 string 的值初始化的新的 Buffer 实例
const b2 = Buffer.from('10', 'utf8');
const b3 = Buffer.from([10]); //返回一个被 array 的值初始化的新的 Buffer 实例(注意⚠️: 传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)
const b4 = Buffer.from(b3);   //复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例

console.log(b1, b2, b3, b4); // <Buffer 31 30> <Buffer 31 30> <Buffer 0a> <Buffer 0a>

⏰ Buffer.alloc()

返回一个已初始化的 Buffer,可以保证新创建的 Buffer 永远不会包含旧数据。

const buffer = Buffer.alloc(10); // 创建一个大小为 10 个字节的缓冲区
console.log(buffer);  // <Buffer 00 00 00 00 00 00 00 00 00 00>

⏰ Buffer.allocUnsafe()

创建一个大小为 size 字节的新的未初始化的 Buffer,由于 Buffer 是未初始化的,因此分配的内存片段可能包含敏感的旧数据。在 Buffer 内容可读情况下,则可能会泄露它的旧数据,这个是不安全的,使用时要谨慎。

const buffer = Buffer.allocUnsafe(10);
console.log(buffer)  //<Buffer 00 00 00 00 00 00 00 00 38 7e>

Buffer 字符编码

通过使用字符编码,可实现 Buffer 实例与 JavaScript 字符串之间的相互转换,目前所支持的字符编码如下所示:

  • 'ascii' - 仅适用于 7 位 ASCII 数据。此编码速度很快,如果设置则会剥离高位。
  • 'utf8' - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8。
  • 'utf16le' - 2 或 4 个字节,小端序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  • 'ucs2' - 'utf16le' 的别名。
  • 'base64' - Base64 编码。当从字符串创建 Buffer 时,此编码也会正确地接受 RFC 4648 第 5 节中指定的 “URL 和文件名安全字母”。
  • 'latin1' - 一种将 Buffer 编码成单字节编码字符串的方法(由 RFC 1345 中的 IANA 定义,第 63 页,作为 Latin-1 的补充块和 C0/C1 控制码)。
  • 'binary' - 'latin1' 的别名。
  • 'hex' - 将每个字节编码成两个十六进制的字符。
const buf = Buffer.from('hello world', 'ascii');
console.log(buf.toString('hex')); // 68656c6c6f20776f726c64

字符串与 Buffer 类型互转

⏰ 字符串转 Buffer

这个相信不会陌生了,通过上面讲解的 Buffer.form() 实现,如果不传递 encoding 默认按照 UTF-8 格式转换存储

const buf = Buffer.from('Node.js 技术栈', 'UTF-8');
console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17

⏰ Buffer 转换为字符串

Buffer 转换为字符串也很简单,使用 toString([encoding], [start], [end]) 方法,默认编码仍为 UTF-8,如果不传 start、end 可实现全部转换,传了 start、end 可实现部分转换(这里要小心了)

const buf = Buffer.from('Node.js 技术栈', 'UTF-8');
console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 9)); // Node.js �

运行查看,可以看到以上输出结果为 Node.js � 出现了乱码,为什么?

❓ 转换过程中为什么出现乱码?

首先以上示例中使用的默认编码方式 UTF-8,问题就出在这里一个中文在 UTF-8 下占用 3 个字节, 这个字在 buf 中对应的字节为 8a 80 e6 而我们的设定的范围为 0~9 因此只输出了 8a,这个时候就会造成字符被截断出现乱码。

下面我们改下示例的截取范围:

const buf = Buffer.from('Node.js 技术栈', 'UTF-8');

console.log(buf); // <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length); // 17
console.log(buf.toString('UTF-8', 0, 11)); // Node.js 技

可以看到已经正常输出了

Json与 Buffer 类型互转

⏰ Json转 Buffer

先把Json对象转成字符串,再通过字符串转成Buffer

const person = {
    name: 'Buffer',
    age: 18   
}
const personStr = JSON.stringify(person)
const buffer = Buffer.from(personStr)
console.log(buffer);

⏰ Buffer转Json

const buf = Buffer.from([0x1, 0x2, 0x3]);
const json = buf.toJSON();

console.log(json); // { type: 'Buffer', data: [ 1, 2, 3 ] }

三、Buffer其它用法

1. Buffer 与迭代器

Buffer 可以使用 for..of 进行迭代:

const buf = Buffer.from([1, 2, 3]);
for (const b of buf) {
  console.log(b); // 1, 2, 3
}

还可以使用 buf.values()、buf.keys()、与 buf.entries() 创建迭代器。

⏰ buf.values()

var buf = Buffer.from('abcdef');
for (x of buf.values()) {
    console.log(x); // 97 98 99 100 101 102
}
⏰ buf.keys()
var buf = Buffer.from('abcdef');
for (x of buf.keys()) {
    console.log(x); // 0 1 2 3 4 5
}
⏰  buf.entries() 
var buf = Buffer.from('abcdef');
for (x of buf.entries()) {
    console.log(x); // [ 0, 97 ] [ 1, 98 ] [ 2, 99 ] [ 3, 100 ] [ 4, 101 ] [ 5, 102 ]
}

2. Buffer的合并(concat)

语法

Buffer.concat(list[, totalLength])

参数

  • list - 用于合并的 Buffer 对象数组列表。
  • totalLength - 指定合并后Buffer对象的总长度。

返回值:返回一个多个成员合并的新 Buffer 对象。

let buffer1 = Buffer.from(('快到碗里来!'));
let buffer2 = Buffer.from(('你才到碗里去!'));
let buffer3 = Buffer.concat([buffer1, buffer2]);
console.log("buffer3 内容: " + buffer3.toString()); //buffer3 内容: 快到碗里来!你才到碗里去!

3.Buffer的比较(compare)

语法

buf.compare(otherBuffer);

参数

otherBuffer - 与 buf 对象比较的另外一个 Buffer 对象。

返回值

返回一个数字,表示 buf 大于 otherBuffer ,小于otherBuffer或者等于otherBuffer。(大于是什么概念呢,其实就是跟C语言的ASCII码值的比较一样,从两个对象的第零个位置开始比较,直到某一位的ASCII码的值大于或者小于另一个对象那一位的ASCII码的值,那么就说这个对象大于或小于另一个对象,否则这两个对象相等)

let buffer1 = Buffer.from('1234');
let buffer2 = Buffer.from('1324');
let result = buffer1.compare(buffer2);

if (result < 0) {
    console.log(buffer1 + " 小于 " + buffer2);
} else if (result == 0) {
    console.log(buffer1 + " 等于 " + buffer2);
} else {
    console.log(buffer1 + " 大于 " + buffer2);
}

//result: 1234 小于 1324

4. Buffer的拷贝(copy)

语法

buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])

参数

  • targetBuffer - 要拷贝的 Buffer 对象。
  • targetStart - 数字, 可选, 默认: 0
  • sourceStart - 数字, 可选, 默认: 0
  • sourceEnd - 数字, 可选, 默认: buffer.length

返回值:没有返回值

let buf1 = Buffer.from('abcdefghijkl');
let buf2 = Buffer.from('0123456');
//将 buf2的第3到(5-1)位 从 buf1 的第2位开始插入
buf2.copy(buf1, 2,3,5);
console.log(buf1.toString());

⏰ 执行结果如下:

5. Buffer的裁剪(slice)

语法

buf.slice([start[, end]])

参数

  • start - 数字, 可选, 默认: 0
  • end - 数字, 可选, 默认: buffer.length

返回值

返回一个新的缓冲区,它和旧缓冲区指向同一块内存,但是从索引 start 到 end 的位置剪切。

let buffer1 = Buffer.from('123456');
console.log('buffer1:', buffer1); //buffer1: <Buffer 31 32 33 34 35 36>
// 剪切缓冲区
let buffer2 = buffer1.slice(0, 2);
console.log("buffer2 content: " + buffer2.toString()); //buffer2 content: 12
buffer2.write('hi', 'utf8'); console.log('修改buffer2的内容后buffer1和buffer2的显示如下'); console.log('buffer1:', buffer1); //buffer1: <Buffer 68 69 33 34 35 36> console.log('buffer2:', buffer2); //buffer2: <Buffer 68 69>

posted on 2024-07-17 16:09  梁飞宇  阅读(46)  评论(0)    收藏  举报