背景

Node.js中有一些我们平常很少直接用到的核心模块,比如Buffer、Stream等。记得我来公司面试的时候,洋彬大佬问我关于这些核心模块的问题,我只能在一边瑟瑟发抖。什么Buffer、Stream、二进制数据之类的概念对于我们这些非计算机专业毕业的同学来说就是——这TM是啥。大多数的教程都是教我们怎么开发WEB应用,讲到这些概念时就直接跳过了,然后告诉你“没事,你用不上的,呵呵”,为了让自己遇到这类问题心里不那么慌,我觉得还是非常有必要了解一下。

这篇文章不会讲很底层的东西(主要原因还是我不懂),如果要了解底层的话,可以看下Page大佬这篇关于Buffer的内存分配、回收的文章(Node中Buffer的初始化及回收)。我想从概念上简单讲讲自己的理解,如果有说得不对的地方,欢迎大家拍砖。

首先,我们看看Node.js官方文档对Buffer的描述:

...mechanism for reading or manipulating streams of binary data.   
The Buffer class was introduced as part of the Node.js API to enable interaction   
with octet streams in TCP streams, file system operations, and other contexts.

额,黑话太多,简化一下就是:Node.js通过Buffer类提供了可以操作二进制数据流的API。还是一头雾水,是吧,那我们一个一个来看。

二进制数据

我们都知道,计算机用二进制来存储和表示数据,所谓二进制数据就是由一系列0和1组成的集合,如:
10 11 001 1110 00101011 是五个不同的0和1的集合,其中的每一个0或1被称为位(BIT,Binary digIT的缩写)。

当计算机要存储或者表示一块数据时,它需要先把这块数据转化成二进制,例如:我们要存储一个数字10,计算机会把数字10转化成1010,至于十进制到二进制的转化方法,想必大家都清楚,这里不再赘述。但是除了存储数字以外,我们可能还需要存储字符串文本、图片、视频等其他格式的数据,计算机以什么样的规则来将这些格式的数据转换成二进制呢?

以字符为例,假设计算机要把一个字符"L"转换成二进制,首先计算机需要将这个字符用数字来表示,然后再把这个数字转换成二进制数据。那"L"应该用什么数字来表示呢?打开浏览器的控制台,输入"L".charCodeAt(0)回车,我们可以看到输出为76,这个76就是"L"的数字表示,也称作字符编码或码点。那么问题来了,计算机怎么知道用什么数字来表示每一个字符呢?为什么要用76来表示字母"L"?字符集就是干这个事情的。

字符集

字符集是一种规则,这种规则规定了每个字符用什么数字来表示。不同的字符集有不同的规则,比较常见的有UnicodeASCII。刚刚我们在浏览器里面输出的76就是"L"的Unicode表示。

现在我们知道了计算机怎么用数字来表示字符,数字有了,怎么把这个数字转换成二进制,是不是直接把十进制的76用二级制的方式1001100来表示就搞定了,不慌,我们来看一下字符编码。

字符编码

字符编码字符集一样,也是一种规则,这种规则规定了怎么把数字表示为二进制,更确切的说是规定了用多少个BIT来表示数字。比如常见的UTF-8就是一种字符编码,它规定单个字符必须用一个字节(BYTE)来表示,也就是8位(BIT),那么76就应该转换为01001100, 10应该转换为00001010,不足8位的要补0

以上描述了计算机怎样以二进制来存储和表示字符、数字,同样地,对于图片、视频,甚至是我们自定义的数据类型,计算机也有相应的编码规则来讲他们转换成二进制。总之,这些被转换之后的数据就是二进制数据。那什么是二进制数据流呢?

Stream流

Node.js中Stream(流)简单来说就是一段数据随着时间的推移从一个地方移动到另一个地方,就像河里的水一样,从一处流到另一处。有了这个东西,我们在处理很大一块数据的时候就不用等着数据全部可用以后才开始处理,大块的数据会被切割成一小块一小块的发送出去,比如:上传一个文件到服务器。

那Buffer在数据流中扮演了怎样的角色,它是如何帮助我们操作数据流的?

Buffer

从上一小节我们知道Stream就是数据从一个地方流动到另一个地方,数据的移动是为了让程序来处理或读取,但是在某些情况下程序没有办法处理,如:

  • 当前到达的数据太少,程序无法解析出完整的信息,需要等待更多的数据到达
  • 程序太忙,已经到达的数据没有时间处理

那在这种情况下,这些未处理的数据需要保存到某个等待区。这个等待区就是Buffer,它的作用就是临时保存这些数据(通常情况下Buffer处于内存中),最终被程序处理掉。

我们可以把StreamBuffer想象成乘客和汽车客运站。客运站有这样两个规定:

  • 当汽车满座时可以及时发车,否则需要等到预定时间才能发车
  • 当运力不足,汽车不够用时,乘客需要等待汽车到来,延迟发车

客运站无法控制乘客到达的速度和数量,反之乘客也无法控制客运站汽车到达的速度和数量。如果乘客来得太早,客运站没有汽车或者不到发车时间,那乘客只能在客运站等待。如果乘客来得慢,汽车一直装不满,那他们也只能等,等到满座或者到达规定时间。

另外一个例子就是在线看视频,如果你的网速很快,那么Buffer内很快就能凑够数帧可以被渲染的数据,程序从Buffer中取出并播放,于此同时Buffer又填充了接下来的数帧数据,等前面的数据播放完以后程序再从Buffer中取数据,如此往复,直到全部视频播放完毕。如果你的网速慢,播放器界面上就会显示一个转圈的菊花,提示缓冲中,这个时候就是Buffer中数据不足,需要等待更多的数据到达。

这……就是Buffer。

Node.js Buffer的官方文档中说,我们可以操作Buffer中的流式数据,这里我挑选几个作为演示。

Buffer的一些API

Node.js内部在处理Stream时会自动创建Buffer,除此以外,我们也可以自己创建Buffer:

	// 创建一个容量为5字节,并且初始化为0的Buffer. 
	const buf1 = Buffer.alloc(5);
	// Prints: <Buffer 00 00 00 00 00>
	console.log(buf);
	// 创建一个指定内容的Buffer
	const buf2 = Buffer.from("hello buffer");
	
	const buf = Buffer.allocUnsafe(10);
	// Prints: (内容不定): <Buffer a0 8b 28 3f 01 00 00 00 50 32>
	console.log(buf);

顺带说一下Buffer.allocBuffer.allocUnsafe的区别。
Buffer模块会预分配一块 **Buffer.poolSize(默认值8192字节)**的缓存池,当使用Buffer.allocUnsafe方法创建Buffer(并且指定的Buffer大小小于Buffer.poolSize的一半)时,
新建的Buffer从缓存池中分配相应内存,优点:速度快,缺点:新分配的内存中可能有脏数据,需要主动清0。Buffer.alloc不会从缓存池请求内存,优点:申请出来的内存干净,缺点:速度慢。

写Buffer: buf.write(string[, offset[, length]][, encoding]),encoding默认为UTF-8

	const buf = Buffer.alloc(256);

	const len = buf.write('\u00bd + \u00bc = \u00be', 0);

	console.log(`${len} bytes: ${buf.toString('utf8', 0, len)}`);
	// Prints: 12 bytes: ½ + ¼ = ¾

以某种格式写Buffer

	const buf = Buffer.allocUnsafe(4);

	buf.writeInt16BE(0x0102, 0);
	buf.writeInt16LE(0x0304, 2);
	console.log(buf);
	// Prints: <Buffer 01 02 04 03>

读Buffer:

	const buf = Buffer.from([0, 0, 0, 5]);

	console.log(buf.readInt32BE(0));
	// Prints: 5
	console.log(buf.readInt32LE(0));
	// Prints: 83886080
	console.log(buf.readInt32LE(1));
	// 越界了
	// Throws ERR_OUT_OF_RANGE
	

回想一下字符集和字符编码的小节,对理解类似writeInt16BE这样的API是不是更清晰了。

完整的API接口请查看官方文档