身为Node开发人员这些年来,你是否从未遇到过Node缓冲区(Buffer)这个概念呢?也许这个术语你见过几次,但每次都不想一探究竟?你可能的确没遇到过要使用缓冲区的场景,毕竟Node.js并不是那种要求程序员直接和程序管理内存的操作打交道的语言。但是,如果你想要成为专业Node开发人员,愿意为此付出更多的努力,那么你就必须深入探索缓冲区之类的概念,从而理解Node的底层工作机制。
本文最初发布于livecodestram.dev网站,经网站授权由InfoQ中文站翻译并分享。
初看上去,你会觉得Node缓冲区是一个很难理解的主题,但事实并非如此。问题在于你看过的所有在线教程上来就会创建一些Node缓冲区,然后开始操作它们,却没有提前解释到底这是什么东西。为了避免在这篇文章中犯同样的错误,我会首先解释什么是缓冲区。但在此之前,我们必须搞明白伴随缓冲区出现的其他一些概念。
为了正确理解缓冲区,我们应该了解二进制数据、字符编码(character encoding)和流(stream)。你可能还不明白它们和缓冲区有什么关系,先别急,搞懂它们后就能知道缓冲区是什么意思了。
如果你已经知道了什么是二进制数据,则可以直接跳到下一个主题。否则就请读下去,了解什么是二进制数据。
二进制数字系统是类似我们常用的十进制的另一个数字系统。十进制使用0-9的数表示数字,而二进制仅使用0和1表示数字。下面是二进制数的一些示例。
0, 1, 10, 101, 1011011, 1000101
在计算机科学中,二进制数中的每个数字均被视为一个位。8个位合称一个字节。那么计算机科学与二进制有什么关系?计算机使用二进制数字来存储和表示数据。因此,存储在计算机中的每种数据最终都将存储为一组二进制数。我们称这些数据为二进制数据。 为了将所有类型的数据都存储为二进制数据,计算机应该知道如何将它们转换为二进制数。计算机为实现这一目的有很多种机制,下面具体介绍。
将数字转换为二进制数据只是一种数学处理。十进制数字9可以用二进制表示为101,其他整数也有自己的对应。计算机具备自行做这种转换的能力。
对这个问题的简单解释是“每个字符都有一个与之关联的唯一二进制数”。这种唯一编号称为字符的代码点或字符码。你可以在Javascript中使用charCodeAt函数来查找每个字符的字符码。
'a'.charCodeAt() //outputs 97
'A'.charCodeAt() //outputs 65
有两大标准用来为每个字符分配字符码:ASCII和Unicode。无论使用哪种编程语言,它们各自赋予字符的字符码都是一样的。ASCII最多使用7位来表示字符,而Unicode最多使用16位。所以Unicode提供了比ASCII更大的范围,可以表示更多的字符,进而成为了最受欢迎的标准。 计算机将字符转换为二进制数据时,需要做的唯一工作就是查找每个字符的字符点吗?答案是否定的。你还需要执行一个将字符转换为二进制数据的步骤。那就是字符编码。
我之前提到过ASCII最多可以使用7位,而Unicode最多可以使用16位来表示字符。但是计算机用不着一直使用Unicode的全部16位来表示字符。例如,字符“A”可以使用至少7位来表示。如果计算机用前导0填充二进制数,使用全部16位来存储“A”,就是在浪费系统资源。
这里就轮到字符编码登场了。字符编码标准决定了计算机用来表示字符的位数。UTF-8、UTF-16和UTF-32就是其中一些字符编码标准。
UTF-8使用8位(一字节)的块表示字符。它可以使用1-4个字节对所有Unicode字符进行编码。现在,如果计算机使用UTF-8标准对“A”进行编码,则存储的二进制值为01000001,带一个前导0。
这样就完成了将字符转换为二进制数据的过程。将字符串转换为二进制数据时,无非就是将每个字符都转换为二进制数据。计算机将图像、音频和视频数据转换为二进制数据时会用到更多的标准和方法。
现在出现了流的概念。我们来看看它又是什么。
流是从一处移到另一处的数据的集合。这里我们谈论的是二进制数据流,它是从一个地方移动到另一个地方的二进制数据的集合。
一条流中会包含大量数据。但是计算机不必等待流中的所有数据到位也可以开始处理它们。将流发送到某个目的地时,由于数据太多了,因此不会一次发送完流中的全部数据,而是将流分为许多较小的数据块。目标会接收这些块并归拢起来,并在有足够的块可用时开始处理它们。
接收流的目标会以某种方式处理数据——可以是读取、操作或写入数据。但是目标处的数据处理器的能力,限制了它一次可以处理的数据量的上下限。那么,当目标接收到不符合此限制的数据块时会发生什么呢?目标无法丢弃它们。然而,目标可以使用一种机制来存储接收到的块,直到它们可以被处理器处理为止。这里就引入了缓冲区的概念。但首先我们应该知道缓冲区到底是什么,才能进一步理解它们如何帮助存储数据块。
一个缓冲区是计算机内存(通常是RAM)中一处较小的存储空间。在目标处理器准备好处理来自流的数据块之前,缓冲区充当后者的等待区域。
如果目标从流中接收数据的速度快于其处理数据的速度,则这些多余的数据将在缓冲区中“等待”,直到处理器可以处理新的数据为止。如果目标从流中接收数据的速度慢于其处理数据的速度,换句话说,如果当前可用的块数量低于处理器可接受的最小数据量,则这些数据块将在一个缓冲区中“等待”,直到有足够数量的数据可用为止。
所以缓冲区指的就是一处等待区域,流数据在这里等待处理器,直到后者准备好处理数据为止。只要是有流存在的地方,你都会看到后台存在缓冲区来存储尚未处理的数据块。
你可能已经听说过缓冲(buffering)这个概念。当你观看YouTube视频时,有时视频会加载一段时间,然后才会继续播放。这是因为你的浏览器正在等待视频流的更多数据块就位。在浏览器收到足够的数据块之前,它们会存储在这些缓冲区中,并等待处理器处理。这就是“缓冲”这个名词的来历。这也正是Node.js中二进制流会遇到的情况。
当我们尝试在Node程序中读取大文件时,也会发生同样的事情。这里使用的缓冲区会存储通过文件流发送的数据块,直到有足够的数据可用,然后再将其传递给程序。此过程也称为缓冲。
现在,你已经了解了缓冲区的基本概念以及为什么需要它们。但是你可能还想知道Node为什么需要缓冲区。
答案很简单。当你将HTTP请求发送到Web服务器时,该请求会作为TCP流通过网络发送,这是一个二进制数据流。因此,你构建的所有Node服务器都必须处理流和缓冲区。
当你使用fs.readFile()方法读取文件时,它将通过回调或promise返回一个缓冲区对象。
简而言之,Node.js中一些最重要的模块会不断处理缓冲区和缓冲区操作。你可能已经在不知不觉中用过了缓冲区。
Node.js提供了一个Buffer类,可让你轻松创建和操作缓冲区。我们来看看用它能做什么。
//创建一个buffer
let buffer1 = Buffer.alloc(100)
这将创建一个大小为100的缓冲区,这意味着该缓冲区会存储100个字节的零。 你还可以从字符串和整数数组创建缓冲区。
let buffer2 = Buffer.from("I'm learning Node Buffer")
//编码给定字符串并将其存储为二进制数据
let buffer3 = Buffer.from([1, 2, 90, 55])
//将每个整数存储为缓冲区
你可以使用索引访问缓冲区中的每个字节。
buffer2[0] //returns 73 for ‘I’
buffer2.toString() //returns ‘I'm learning Node Buffer’
现在我们来看看如何写入缓冲区。
buffer2.write("Hi")
buffer2.toString() //returns ‘Hi’
write方法将覆盖缓冲区中的现有内容,并将其更改为你提供的值。
//使用指定的索引更改存储的值
buffer2[0] = 80
buffer2.toString() //returns ‘Pi’
//获取缓冲区长度
buffer1.length //returns 100
buffer3.length //returns 4
你可以查看Node.js文档(https://nodejs.org/api/buffer.html),了解还可以使用缓冲区做些什么事情。
如你在本文中所见,缓冲区对于Node.js的工作机制而言至关重要。了解这些概念可以帮助你成为更优秀的Node开发人员。这些知识可帮助你编写优化的Node程序,并了解这种语言的局限性,知道如何解决它们。因此,下次你遇到与Node.js相关的令人生畏的术语时,请不要犹豫,应该像对待缓冲区一样直接面对它。
原文链接:https://livecodestream.dev/post/2020-06-06-a-complete-introduction-to-node-buffers
领取专属 10元无门槛券
私享最新 技术干货