在Node.js中使用缓冲区

作者選擇了COVID-19 救濟基金作為撰寫捐贈計劃的捐款接受方。

介紹

A buffer is a space in memory (typically RAM) that stores binary data. In Node.js, we can access these spaces of memory with the built-in Buffer class. Buffers store a sequence of integers, similar to an array in JavaScript. Unlike arrays, you cannot change the size of a buffer once it is created.

如果您已經編寫過 Node.js 代碼,您可能已經隱式使用了緩衝區。例如,當您使用 fs.readFile() 讀取文件時,返回到回調或 Promise的數據是一個緩衝區對象。此外,當在 Node.js 中發出 HTTP 請求時,如果客戶端無法立即處理數據流,則會將數據流暫存到內部緩衝區中。

在與二進制數據交互時,緩衝區非常有用,通常用於較低級別的網絡操作。它還使您能夠在 Node.js 中進行細粒度的數據操作。

在這個教程中,您將使用 Node.js REPL 來運行各種緩衝區的示例,如創建緩衝區、從緩衝區讀取、向緩衝區寫入和從緩衝區複製,以及使用緩衝區在二進制和編碼數據之間進行轉換。到教程結束時,您將學會如何使用 Buffer 類來處理二進制數據。

先決條件

  • 您需要在開發機器上安裝 Node.js。本教程使用版本 10.19.0。要在 macOS 或 Ubuntu 18.04 上安裝此版本,請按照 在 macOS 上安裝 Node.js 並創建本地開發環境 中的步驟,或者按照 使用 PPA 安裝Ubuntu 18.04 上安裝 Node.js 中的相應部分進行操作。
  • 在本教程中,您將在 Node.js REPL(Read-Evaluate-Print-Loop)中與緩衝區進行交互。如果您想對如何有效使用 Node.js REPL 進行回顧,可以閱讀我們的指南 如何使用 Node.js REPL
  • 對於這篇文章,我們預期用戶能夠熟悉基本的 JavaScript 及其數據類型。您可以通過我們的 JavaScript 編碼系列 來學習這些基礎知識。

步驟 1 — 創建緩衝區

第一步將向您展示在 Node.js 中創建緩衝區對象的兩種主要方法。

要決定使用哪種方法,您需要回答這個問題:您是想創建一個新的緩衝區還是從現有數據中提取一個緩衝區?如果您打算將尚未接收到的數據存儲在內存中,則需要創建一個新的緩衝區。在 Node.js 中,我們使用 Buffer alloc() 函數來執行此操作。

讓我們打開 Node.js REPL 自己看看。在終端中,輸入 node 命令:

  1. node

您將看到提示符開始以 > 開頭。

alloc() 函數將緩衝區的大小作為其第一個且唯一必需的參數。大小是一個整數,表示緩衝區將使用多少字節的內存。例如,如果我們想要創建一個 1KB(千字節)大小的緩衝區,相當於 1024 字節,我們將在控制台中輸入:

  1. const firstBuf = Buffer.alloc(1024);

要創建一個新的緩衝區,我們使用了全局可用的Buffer類,該類具有alloc()方法。通過將1024作為alloc()的參數,我們創建了一個大小為1KB的緩衝區。

默認情況下,當您使用alloc()初始化緩衝區時,該緩衝區將用二進製零填充,作為稍後數據的占位符。但是,如果我們願意,我們可以更改默認值。如果我們想要創建一個新的緩衝區,其中填充了1而不是0,我們將設置alloc()函數的第二個參數—fill

在您的終端中,創建一個在REPL提示符下填充了1的新緩衝區:

  1. const filledBuf = Buffer.alloc(1024, 1);

我們剛創建了一個新的緩衝區對象,該對象引用存儲了1KB 1的內存空間。儘管我們輸入的是整數,但緩衝區中存儲的所有數據都是二進制數據。

二進制數據可以有許多不同的格式。例如,讓我們考慮一個代表一個字節數據的二進制序列:01110110。如果此二進制序列使用ASCII編碼標準表示英文字符串,則它將是字母v。然而,如果我們的計算機正在處理圖像,該二進制序列可能包含有關像素顏色的信息。

電腦知道如何以不同方式處理它們,因為位元組的編碼方式不同。位元組編碼是位元組的格式。在Node.js中,如果使用字符串數據初始化緩衝區,則緩衝區默認使用UTF-8編碼方案。 UTF-8中的一個位元組代表一個數字、一個字母(用於英語和其他語言)或一個符號。UTF-8是ASCII的超集,即美國信息交換標準代碼。 ASCII可以編碼大寫和小寫英文字母、數字0-9以及幾個其他符號,例如驚嘆號(!)或和號(&)。

如果我們正在編寫一個僅能處理ASCII字符的程序,我們可以通過緩衝區的alloc()函數的第三個參數——encoding來更改所使用的編碼。

讓我們創建一個新的緩衝區,它長度為五個位元組,並僅存儲ASCII字符:

  1. const asciiBuf = Buffer.alloc(5, 'a', 'ascii');

緩衝區初始化為五個字符a的位元組表示。

注意: 默認情況下,Node.js支持以下字符編碼:

  • ASCII,表示為ascii
  • UTF-8,表示為utf-8utf8
  • UTF-16,表示為utf-16leutf16le
  • UCS-2,表示為 ucs-2ucs2
  • Base64,表示為 base64
  • Hexadecimal,表示為 hex
  • ISO/IEC 8859-1,表示為 latin1binary

所有這些值都可以用於接受 encoding 參數的 Buffer 類函數。因此,這些值對於 alloc() 方法都是有效的。

到目前為止,我們一直使用 alloc() 函數創建新的緩衝區。但有時我們可能希望從已經存在的數據(如字符串或數組)創建緩衝區。

為了從預先存在的數據創建緩衝區,我們使用 from() 方法。我們可以使用該函數從以下數據創建緩衝區:

  • 一個整數數組:整數值可以介於 0255 之間。
  • 一個 ArrayBuffer:這是一個存儲固定字節長度的 JavaScript 對象。
  • A string.
  • 另一個緩衝區。
  • 其他具有Symbol.toPrimitive属性的JavaScript对象。 该属性告诉JavaScript如何将对象转换为原始数据类型:booleannullundefinednumberstringsymbol。 您可以在Mozilla的JavaScript 文档中阅读更多关于Symbols的信息。

让我们看看如何从字符串创建缓冲区。 在Node.js提示符中,输入以下内容:

  1. const stringBuf = Buffer.from('My name is Paul');

现在我们已经从字符串My name is Paul创建了一个缓冲区对象。 让我们从前面创建的另一个缓冲区创建一个新的缓冲区:

  1. const asciiCopy = Buffer.from(asciiBuf);

现在我们已经创建了一个名为asciiCopy的新缓冲区,其中包含与asciiBuf相同的数据。

现在我们已经体验了创建缓冲区,我们可以深入到读取它们数据的示例中。

步骤2 —— 从缓冲区读取

有许多方法可以访问缓冲区中的数据。 我们可以访问缓冲区中的单个字节,也可以提取整个内容。

要访问缓冲区的一个字节,我们传递要获取的字节的索引或位置。 缓冲区像数组一样按顺序存储数据。 它们也像数组一样索引其数据,从0开始。 我们可以在缓冲区对象上使用数组表示法来获取单个字节。

讓我們通過在 REPL 中從字符串創建緩衝區來查看這個:

  1. const hiBuf = Buffer.from('Hi!');

現在讓我們讀取緩衝區的第一個字節:

  1. hiBuf[0];

當你按下ENTER時,REPL 將顯示:

Output
72

整數72對應於字母H的 UTF-8 表示。

注意: 字節的值可以是介於0255之間的數字。一個字節是一個包含 8 位的序列。一個位是二進制的,因此只能有兩個值:01。如果我們有一個包含 8 個位的序列,每個位有兩個可能的值,那麼一個字節的可能值最多為 2⁸。這意味著最多有 256 個值。由於我們從零開始計數,這意味著我們的最高數字是 255。

讓我們對第二個字節做同樣的操作。在 REPL 中輸入以下內容:

  1. hiBuf[1];

REPL 返回105,代表小寫字母i

最後,讓我們獲取第三個字符:

  1. hiBuf[2];

你會在 REPL 中看到33,對應於!

讓我們嘗試從無效的索引檢索一個字節:

  1. hiBuf[3];

REPL 將返回:

Output
undefined

這就像我們嘗試使用不正確的索引訪問數組中的元素一樣。

既然我們已經看到如何讀取緩衝區的單個字節,那麼讓我們看看檢索緩衝區中所有數據的選項。緩衝區對象附帶了toString()toJSON()方法,它們以兩種不同的格式返回緩衝區的全部內容。

如其名,toString() 方法將緩衝區的位元組轉換為字串並將其返回給使用者。如果我們對hiBuf 使用此方法,我們將獲得字串Hi!。讓我們試試看!

在提示符中,輸入:

  1. hiBuf.toString();

REPL將返回:

Output
'Hi!'

該緩衝區是從字串創建的。讓我們看看如果我們對不是從字串數據創建的緩衝區使用toString()會發生什麼。

讓我們創建一個新的、空的緩衝區,大小為10位元組:

  1. const tenZeroes = Buffer.alloc(10);

現在,讓我們使用toString()方法:

  1. tenZeroes.toString();

我們將看到以下結果:

'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'

字串\u0000NULL的Unicode字符。它對應於數字0。當緩衝區的數據未編碼為字串時,toString()方法將返回位元組的UTF-8編碼。

toString()具有可選參數encoding。我們可以使用此參數來更改返回的緩衝區數據的編碼。

例如,如果您想要hiBuf的十六進制編碼,您可以在提示符中輸入以下內容:

  1. hiBuf.toString('hex');

該語句將求值為:

Output
'486921'

486921是代表字串Hi!的位元組的十六進制表示。在Node.js中,當用戶想要將數據的編碼從一種形式轉換為另一種形式時,他們通常將字串放入緩衝區並使用所需的編碼調用toString()

toJSON() 方法的行为有所不同。无论缓冲区是由字符串还是其他数据创建的,它始终将数据作为字节的整数表示返回。

让我们重复使用 hiBuftenZeroes 缓冲区来练习使用 toJSON()。在提示符下,输入:

  1. hiBuf.toJSON();

REPL 将返回:

Output
{ type: 'Buffer', data: [ 72, 105, 33 ] }

JSON 对象具有一个 type 属性,该属性将始终为 Buffer。这样程序就可以区分这些 JSON 对象和其他 JSON 对象。

data 属性包含字节的整数表示数组。您可能已经注意到,7210533 对应于我们分别提取字节时收到的值。

让我们尝试对 tenZeroes 使用 toJSON() 方法:

  1. tenZeroes.toJSON();

在 REPL 中,您将看到以下内容:

Output
{ type: 'Buffer', data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }

type 与之前相同。但是,数据现在是一个包含十个零的数组。

现在我们已经介绍了从缓冲区读取的主要方法,让我们看看如何修改缓冲区的内容。

步骤 3 — 修改缓冲区

我们可以使用多种方法修改现有的缓冲区对象。与读取类似,我们可以使用数组语法逐个修改缓冲区字节。我们还可以向缓冲区写入新内容,以替换现有数据。

讓我們首先看看如何更改緩衝區的個別位元組。回想一下我們的緩衝區變數hiBuf,其中包含字符串Hi!。讓我們將每個位元組更改為包含Hey

在REPL中,讓我們首先嘗試將hiBuf的第二個元素設置為e

  1. hiBuf[1] = 'e';

現在,讓我們將此緩衝區視為一個字符串,以確認它是否存儲了正確的數據。隨後調用toString()方法:

  1. hiBuf.toString();

它將被評估為:

Output
'H\u0000!'

我們收到了奇怪的輸出,因為緩衝區只能接受整數值。我們不能將它賦予字母e;相反,我們必須將它分配給二進制等效於e的數字:

  1. hiBuf[1] = 101;

現在,當我們調用toString()方法時:

  1. hiBuf.toString();

在REPL中我們得到這個輸出:

Output
'He!'

要更改緩衝區中的最後一個字符,我們需要將第三個元素設置為對應於y字節的整數:

  1. hiBuf[2] = 121;

讓我們再次使用toString()方法來確認:

  1. hiBuf.toString();

您的REPL將顯示:

Output
'Hey'

如果我們嘗試寫入超出緩衝區範圍的位元組,它將被忽略,緩衝區的內容將不會更改。例如,讓我們嘗試將不存在的緩衝區的第四個元素設置為o

  1. hiBuf[3] = 111;

我們可以使用toString()方法確認緩衝區未更改:

  1. hiBuf.toString();

輸出仍然是:

Output
'Hey'

如果我們想要更改整個緩衝區的內容,我們可以使用write()方法。 write()方法接受一個字符串,將替換緩衝區的內容。

讓我們使用write()方法將hiBuf的內容更改回Hi!。在您的Node.js shell中,在提示符號處輸入以下命令:

  1. hiBuf.write('Hi!');

REPL中的write()方法返回3。這是因為它寫入了三個字節的數據。由於該緩衝區使用UTF-8編碼,每個字母的大小為一個字節,因為UTF-8編碼使用一個字節表示每個字符。如果緩衝區使用UTF-16編碼,每個字符至少有兩個字節,那麼write()函數將返回6

現在通過使用toString()來驗證緩衝區的內容:

  1. hiBuf.toString();

REPL將產生:

Output
'Hi!'

這比逐字節更改每個元素要快。

如果嘗試寫入的字節超過緩衝區的大小,則緩衝區對象將僅接受符合大小的字節。為了說明,讓我們創建一個存儲三個字節的緩衝區:

  1. const petBuf = Buffer.alloc(3);

現在讓我們嘗試將Cats寫入其中:

  1. petBuf.write('Cats');

當評估write()調用時,REPL返回3,表示只有三個字節被寫入緩衝區。現在確認緩衝區包含前三個字節:

  1. petBuf.toString();

REPL返回:

Output
'Cat'

相比之下,讓我們創建一個存儲四個字節的Buffer:

  1. const petBuf2 = Buffer.alloc(4);

將相同的內容寫入其中:

  1. petBuf2.write('Cats');

然後添加一些佔用比原始內容更少空間的新內容:

  1. petBuf2.write('Hi');

由於緩衝區是按順序寫入的,從0開始,如果我們打印緩衝區的內容:

  1. petBuf2.toString();

我們將看到:

Output
'Hits'

前兩個字符被覆蓋,但緩衝區的其餘部分保持不變。

有時我們想要放入現有緩衝區的數據並不在字符串中,而是位於另一個緩衝區對象中。在這些情況下,我們可以使用copy()函數來修改我們的緩衝區存儲的內容。

讓我們創建兩個新的緩衝區:

  1. const wordsBuf = Buffer.from('Banana Nananana');
  2. const catchphraseBuf = Buffer.from('Not sure Turtle!');

wordsBufcatchphraseBuf緩衝區都包含字符串數據。我們想要修改catchphraseBuf,使其存儲Nananana Turtle!而不是Not sure Turtle!。我們將使用copy()wordsBuf複製NanananacatchphraseBuf

要從一個緩衝區複製數據到另一個緩衝區,我們將在提供信息的緩衝區上使用copy()方法。因此,由於wordsBuf包含我們想要複製的字符串數據,我們需要這樣複製:

  1. wordsBuf.copy(catchphraseBuf);

這種情況下的target參數是catchphraseBuf緩衝區。

當我們將其輸入到 REPL 中時,它返回 15,表示寫入了 15 個位元組。 字串 Nananana 僅使用了 8 個位元組的資料,所以我們立即知道我們的複製並未如預期那樣進行。 使用 toString() 方法來查看 catchphraseBuf 的內容:

  1. catchphraseBuf.toString();

REPL 返回:

Output
'Banana Nananana!'

默認情況下,copy() 採取了 wordsBuf 的整個內容並將其放入 catchphraseBuf 中。 我們需要更有選擇性地完成我們的目標,只複製 Nananana。 在繼續之前,讓我們重新編寫 catchphraseBuf 的原始內容:

  1. catchphraseBuf.write('Not sure Turtle!');

copy() 函數有幾個更多的參數,允許我們自定義複製到其他緩衝區的資料。 這是此函數的所有參數的列表:

  • targetcopy() 的唯一必需參數。 如我們之前的使用所示,它是我們想要複製到的緩衝區。
  • targetStart – 這是我們應該從目標緩衝區中的位元組索引開始複製的位置。 默認情況下為 0,表示從緩衝區的開頭開始複製資料。
  • sourceStart – 這是我們應該從源緩衝區中的位元組索引開始複製的位置。
  • sourceEnd – 這是我們應該停止複製的源緩衝區中的位元組索引。 默認情況下,它是緩衝區的長度。

所以,要從wordsBuf中複製NanananacatchphraseBuf,我們的target應該像以前一樣是catchphraseBuftargetStart將是0,因為我們希望Nananana出現在catchphraseBuf的開頭。 sourceStart應該是7,因為這是wordsBufNananana開始的索引。 sourceEnd將繼續是緩衝區的長度。

在REPL提示符號下,像這樣複製wordsBuf的內容:

  1. wordsBuf.copy(catchphraseBuf, 0, 7, wordsBuf.length);

REPL確認已經寫入8個字節。注意如何將wordsBuf.length用作sourceEnd參數的值。與數組一樣,length屬性給了我們緩衝區的大小。

現在讓我們看看catchphraseBuf的內容:

  1. catchphraseBuf.toString();

REPL返回:

Output
'Nananana Turtle!'

成功!我們能夠通過複製wordsBuf的內容來修改catchphraseBuf的數據。

如果您願意,可以退出Node.js REPL。請注意,當您這樣做時,所有已創建的變量將不再可用:

  1. .exit

結論

在本教程中,您学习到缓冲区是内存中的固定长度分配,用于存储二进制数据。您首先通过在内存中定义它们的大小并使用预先存在的数据进行初始化来创建缓冲区。然后,您通过检查其单个字节并使用toString()toJSON()方法从缓冲区中读取数据。最后,您通过更改其单个字节并使用write()copy()方法修改了缓冲区中存储的数据。

缓冲区使您深入了解了Node.js如何操作二进制数据。现在您可以与缓冲区交互,观察字符编码影响数据存储的不同方式。例如,您可以从不是UTF-8或ASCII编码的字符串数据创建缓冲区,并观察它们在大小上的差异。您还可以使用toString()将带有UTF-8的缓冲区转换为其他编码方案。

要了解有关Node.js中缓冲区的信息,您可以阅读Node.js文档关于Buffer对象的部分。如果您想继续学习Node.js,可以返回到如何在Node.js中编码系列,或者在我们的Node主题页面上浏览编程项目和设置。

Source:
https://www.digitalocean.com/community/tutorials/using-buffers-in-node-js