Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

Введение

Буфер — пространство в памяти (как правило, оперативной), в котором хранятся бинарные данные. В Node.js мы можем получить доступ к этим пространствам памяти со встроенным классом Buffer. Буферы хранят последовательность целых чисел, аналогично массиву в JavaScript. В отличие от массивов, вы не можете изменять размер буфера после его создания.

Возможно, вы уже опосредованно использовали буферы, если вы уже написали код Node.js. Например, когда вы читаете данные из файла с fs.readFile(), данные, возвращаемые в обратный вызов или Promise, являются буферным объектом. Кроме того, когда запросы HTTP поступают в Node.js, возвращаются потоки данных, которые временно хранятся во внутреннем буфере, когда пользователь не может обработать весь поток сразу.

Буферы управления полезны при взаимодействии с бинарными данными — как правило, на низком уровне сетей. Также они дают возможность осуществлять точную манипуляцию данными в Node.js.

В этом обучающем вы будете использовать Node.js REPL для работы с различными примерами буферов — создание буферов, чтение из буферов, запись данных в буферы и копирование из буферов, а также использование буферов для конвертации между бинарными и кодированными данными. В конце этого обучающего модуля вы научитесь использовать класс Buffer для работы с бинарными данными.

Предварительные требования

  • Вам потребуется установить Node.js на вашем рабочем компьютере. В этом обучающем руководстве мы используем версию 10.19.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04.
  • В этом обучающем руководстве вы будете работать с буферами в Node.js REPL (чтение-оценка-печать-цикл). Если хотите освежить знания по эффективному использованию Node.js REPL, почитайте наше руководство Как использовать Node.js REPL.
  • Для этой статьи мы предполагаем, что пользователю будет удобнее использовать базовый JavaScript и его типы данных. Вы можете получить эти базовые знания в нашей серии Программирование на JavaScript.

Шаг 1 — Создание буфера

На первом шаге вы увидите два основных способа создания буферного объекта в Node.js.

Для выбора нужного метода необходимо ответить на вопрос: Вы хотите создать новый буфер или извлечь буфер из существующих данных? Если вы хотите хранить в памяти данные, которые еще не получили, то вам нужно создать новый буфер. Для этого в Node.js мы используем функцию alloc() класса Buffer.

Давайте откроем Node.js REPL и посмотрим. В терминале введите команду node:

  • node

Появится командная строка, начинающаяся с >.

Функция alloc() принимает размер буфера в качестве первого и единственного аргумента. Размер представляет собой целое число, указывающее, сколько байтов памяти будет использовать буферный объект. Например, если бы мы захотели создать буфер размером 1КБ (килобайт), что эквивалентно 1024 байтам, то мы бы ввели в консоль следующее:

  • const firstBuf = Buffer.alloc(1024);

Для создания нового буфера мы использовали глобально доступный класс Buffer, который имеет метод alloc(). Предоставив 1024 в качестве аргумента для alloc(), мы создали буфер размером 1 КБ.

По умолчанию при инициализации буфера с помощью alloc() буфер заполнен бинарными нулями в качестве заполнителя для дальнейших данных. Но при желании мы можем изменить значение по умолчанию. Если бы мы захотели создать новый буфер с единицами вместо нулей, то мы бы настроили второй параметр функции alloc() — fill.

Создайте в терминале новый буфер (при помощи командной строки диалога REPL), заполненный единицами:

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

Мы только что создали новый буферный объект, указывающий на пространство в памяти, в котором хранится 1КБ единиц. Хотя мы ввели целое число, все данные, хранимые в буфере, являются бинарными.

Бинарные данные могут быть представлены самыми различными форматами. Например, рассмотрим бинарную последовательность, представляющую байт данных: 01110110. Если бы эта бинарная последовательность представляла строку на английском языке с использованием стандарта кодирования ASCII, то это была бы буква v. Но если бы наш компьютер обрабатывал изображение, то эта бинарная последовательность могла бы содержать информацию о цвете пикселя.

Компьютер понимает, что их необходимо обрабатывать по-разному, т.к. байты закодированы по-разному. Кодирование байта — это форма байта. Буфер в Node.js использует систему кодирования UTF-8 по умолчанию, если она инициализируется со строковыми данными. Байт в UTF-8 представляет собой число, букву (на английском и других языках) или символ. UTF-8 — надмножество ASCII (Американский стандартный код информационного обмена). ASCII может кодировать байты с прописными и строчными английскими буквами, цифрами 0-9, и некоторыми другими символами — например, восклицательным знаком (!) или амперсандом (&).

Если бы мы писали программу, способную работать только с символами ASCII, то мы бы смогли изменить кодировку, используемую нашим буфером, при помощи третьего аргумента функции alloc() — encoding.

Создадим новый буфер длиной пять байтов для хранения только символов ASCII:

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

Буфер инициализируется с пятью байтами символа а при помощи представления ASCII.

Примечание. По умолчанию Node.js поддерживает следующие кодировки символов:

  • ASCII, представленная как ascii
  • UTF-8, представленная как utf-8 или utf8
  • UTF-16, представленная как utf-16le или utf16le
  • UCS-2, представленная как ucs-2 или ucs2
  • Base64, представленная как base64
  • Hexadecimal, представленная как hex
  • ISO/IEC 8859-1, представленная как latin1 или binary

Все эти значения можно использовать в функциях класса Buffer, принимающих параметр кодирования. Таким образом, все эти значения действительны для метода alloc().

Пока что мы создавали новые буферы с помощью функции alloc(). Но иногда нам может потребоваться создать буфер из уже существующих данных, например строки или массива.

Для создания буфера из уже существовающих данных мы используем метод from(). Мы можем использовать эту функцию для создания буферов из следующего:

  • Массив целых чисел: целые значения могут составлять от 0 до 255.
  • ArrayBuffer: это объект JavaScript, хранящий фиксированную длину байтов.
  • Строка.
  • Еще один буфер.
  • Другие объекты JavaScript, имеющие свойство Symbol.toPrimitive. Это свойство указывает для JavaScript, как конвертировать объект в примитивный тип данных: булев, пустой, неопределенный, число, строка или символ. Дополнительную информацию о Symbols можно найти в документации JavaScript сообщества Mozilla.

Посмотрим, как можно создать буфер из строки. В диалоге Node.js введите следующее:

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

Теперь у нас есть буферный объект, созданный из строки My name is Paul. Создадим новый буфер из другого буфера, который мы создали ранее:

  • const asciiCopy = Buffer.from(asciiBuf);

Мы создали новый буфер asciiCopy, содержащий те же данные, что и asciiBuf.

Теперь, когда мы получили опыт создания буферов, мы можем переходить к примерам чтения их данных.

Шаг 2 — Чтение из буфера

Есть много способов доступа к данным в буфере. Мы можем получить доступ к отдельному байту в буфере, а также мы можем извлечь все содержимое.

Для доступа к одному байту буфера мы передаем индекс или местоположение требуемого байта. Буферы хранят данные последовательно, в виде массивов. Они также индексируют свои данные в виде массивов, начиная с 0. Мы можем использовать обозначение массива в объекте буфера для получения отдельного байта.

Посмотрим, как это выглядит, создав буфер из строки в REPL:

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

Теперь прочитаем первый байт этого буфера:

  • hiBuf[0];

При нажатии ENTER REPL отобразит следующее:

Output
72

Целое число 72 соответствует представлению UTF-8 для буквы H.

Примечание: значения для байтов могут быть числами от 0 до 255. Байт — последовательность из 8 бит. Бит — бинарная единица данных, поэтому он может иметь только два значения: 0 или 1. Если у нас есть последовательность из 8 битов и двух возможных значений для бита, то у нас есть максимум 2⁸ возможных значений байта. Таким образом, в байте может быть максимум 256 чисел. Поскольку мы начинаем считать с нуля, это означает, что наше самое высокое число — 255.

Давайте сделаем то же самое для второго байта. Введите в REPL следующее:

  • hiBuf[1];

REPL возвращает 105, что представляет собой строчную букву i.

Наконец, получим третий символ:

  • hiBuf[2];

Вы увидите число 33, отображаемое в REPL, что соответствует символу !.

Давайте попробуем извлечь байт из недействительного индекса:

  • hiBuf[3];

REPL выдаст следующее:

Output
undefined

Это похоже на то, как если бы мы попытались получить доступ к элементу в массиве с неверным индексом.

Теперь, когда мы увидели, как читать отдельные байты буфера, рассмотрим наши варианты одновременного получения всех данных, хранимых в буфере. Объект буфера имеет методы toString() и toJSON(), возвращающие все содержимое буфера в двух разных форматах.

Как и предполагает его название, метод toString() конвертирует байты буфера в строку и возвращает их пользователю. Если мы используем этот метод в hiBuf, то получаем строку Hi!. Давайте попробуем.

В диалоге введите следующее:

  • hiBuf.toString();

REPL выдаст следующее:

Output
'Hi!'

Этот буфер был создан из строки. Посмотрим, что произойдет, если мы используем toString() в буфере, который не был составлен из строковых данных.

Создадим новый пустой буфер размером 10 байтов.

  • const tenZeroes = Buffer.alloc(10);

Теперь используем метод toString():

  • tenZeroes.toString();

Мы увидим следующий результат:

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

Строка \u0000 представляет собой символ Unicode для NULL. Он соответствует числу 0. Когда данные буфера не кодируются как строка, метод toString() возвращает кодирование UTF-8 байтов.

toString() имеет опциональный параметр — encoding (кодировка) Мы можем использовать этот параметр для изменения кодирования возвращаемых данных буфера.

Например, если вы хотите шестнадцатеричную кодировку hiBuf , то необходимо ввести в диалоге следюущее:

  • hiBuf.toString('hex');

Это выражение даст следующее:

Output
'486921'

486921 — это шестнадцатеричное представление байтов, представляющих строку Hi!. В Node.js, когда пользователи хотят конвертировать кодирование данных из одной формы в другую, то обычно они ставят строку в буфер и вызывают toString() с необходимым кодированием.

Метод toJSON() ведет себя по-разному. Независимо от того, был ли создан буфер из строки или нет, он всегда возвращает данные как целочисленное представление байта.

Давайте снова используем буферы hiBuf и tenZeroes, чтобы потренироваться с JSON(). В диалоге введите следующее:

  • hiBuf.toJSON();

REPL выдаст следующее:

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

Объект JSON имеет свойство type, которое всегда будет иметь класс Buffer. Это сделано для того, чтобы программы могли отличать эти объекты JSON от других объектов JSON.

Свойство data (данные) содержит массив целочисленного представления байтов. Возможно, вы заметили, что 72, 105 и 33 соответствуют значениям, полученным нами при индивидуальном выводе байтов.

Давайте попробуем метод toJSON() с tenZeroes:

  • tenZeroes.toJSON();

В REPL вы увидите следующее:

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

Тип — такой же, как и ранее. Но теперь данные являются массивом с десятью нулями.

Теперь, когда мы рассмотрели основные способы чтения из буфера, посмотрим, как можно изменять содержимое буфера.

Шаг 3 — Изменение буфера

Есть много способов изменения существующего буферного объекта. Как и в случае с чтением, мы можем изменять байты в буфере по отдельности с использованием синтаксиса массива. Также мы можем записывать новое содержимое в буфер, заменяя существующие данные.

Давайте начнем с того, как можно изменять отдельные байты буфера. Вспомним нашу переменную буфера hiBuf, содержащую строку Hi!. Давайте изменим каждый байт, чтобы он вместо этого содержал Hey.

В REPL сначала попробуем задать второй элемент hiBuf как e:

  • hiBuf[1] = 'e';

Теперь рассмотрим этот буфер в качестве строки для подтверждения того, что он хранит нужные данные. Далее вызовите метод toString():

  • hiBuf.toString();

Его значение будет таким:

Output
'H\u0000!'

Мы получили такое странное значение, поскольку буфер может принимать только целые числа. Мы не можем присвоить его букве e; вместо этого, нам нужно присвоить ему число, чей бинарный эквивалент представляет собой e:

  • hiBuf[1] = 101;

Теперь, когда мы вызываем метод toString():

  • hiBuf.toString();

Мы получаем следующее в REPL:

Output
'He!'

Для изменения последнего символа в буфере нам нужно задать третий элемент в виде целого числа, соответствующего байту y:

  • hiBuf[2] = 121;

Давайте подтвердим, используя метод toString() еще раз:

  • hiBuf.toString();

Ваш REPL отобразит следующее:

Output
'Hey'

Если мы попытаемся написать байт, который находится вне диапазона буфера, то он будет проигнорирован, а содержимое буфера не изменится. Например, попробуем задать несуществующий четвертый элемент буфера, чтобы:

  • hiBuf[3] = 111;

Мы можем подтвердить, что буфер не изменился с помощью метода toString():

  • hiBuf.toString();

В результате по-прежнему получаем:

Output
'Hey'

Если захотим изменить содержимое всего буфера, то можем использовать метод write(). Метод write() принимает строку, которая заменит содержимое буфера.

Давайте используем метод write() для изменения содержимого hiBuf обратно в Hi!. В оболочке Node.js введите в диалоге следующую команду:

  • hiBuf.write('Hi!');

Метод write() выдал результат 3 в REPL. Это связано с тем, что он записал три байта данных. Каждая буква имеет размер в один байт, поскольку в этом буфере используется кодировка UTF-8, где каждому символу соответствует один байт. Если бы в буфере использовалась кодировка UTF-16, где предусмотрено минимум два байта на символ, то функция write() выдала бы результат 6.

Теперь проверьте содержимое буфера, используя toString():

  • hiBuf.toString();

REPL выдаст следующее:

Output
'Hi!'

Это быстрее, чем изменять каждый элемент по отдельным байтам.

Если вы попробуете записать больше байтов, чем вмещается в буфер, то буферный объект будет принимать только то, на что хватает байтов. Для иллюстрации создадим буфер, в котором хранится три байта:

  • const petBuf = Buffer.alloc(3);

Теперь попробуем написать в него слово Cats:

  • petBuf.write('Cats');

Когда оценивается вызов write(), REPL возвращает 3 — это указывает на то, что в буфер было записано только три байта. Теперь убедитесь, что буфер содержит первые три байта:

  • petBuf.toString();

REPL выдает следующее:

Output
'Cat'

Функция write() добавляет байты в последовательном порядке, поэтому только первые три байта были помещены в буфер.

Для сравнения создадим буфер, в котором хранится четыре байта:

  • const petBuf2 = Buffer.alloc(4);

Запишем в него то же самое содержимое:

  • petBuf2.write('Cats');

Затем добавим какое-нибудь новое содержимое, которое занимает меньше места, чем первоначальное содержимое:

  • petBuf2.write('Hi');

Поскольку буферы записывают данные последовательно, начиная с 0, если мы распечатаем содержимое буфера:

  • petBuf2.toString();

Мы увидим приветствие:

Output
'Hits'

Первые два символа перезаписаны, но остальная часть буфера не затронута.

Иногда данные, которые мы хотим иметь в нашем ранее созданном буфере, находятся не в строке, а в другом буферном объекте. В таких случаях мы можем использовать функцию copy() для изменения того, что хранится в нашем буфере.

Давайте создадим два новых буфера:

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

Буферы wordsBuf и catchphraseBuf содержат строковые данные. Мы хотим изменить catchphraseBuf так, чтобы в нем хранились слова Nananana Turtle! вместо слов Not sure Turtle!. Мы используем copy() для получения Nananana из wordsBuf в catchphraseBuf.

Для копирования данных из одного буфера в другой мы используем метод copy() в том буфере, который является источником информации. Поэтому, поскольку wordsBuf имеет строковые данные, которые мы хотим скопировать, нам нужно копировать следующим образом:

  • wordsBuf.copy(catchphraseBuf);

Параметр target в данном случае — catchphraseBuf.

Когда мы вводим это в REPL, получаем результат 15, указывающий на то, что было записано 15 байтов. Строка Nananana использует только 8 байтов данных, поэтому мы сразу же узнаем, что наша копия не была отправлена так, как было необходимо. Используйте метод toString() для просмотра содержимого catchphraseBuf:

  • catchphraseBuf.toString();

REPL выдает следующее:

Output
'Banana Nananana!'

По умолчанию copy() приняла все содержимое wordsBuf и поместила его в catchphraseBuf. Нам нужно быть более избирательными для нашей задачи и копировать только Nananana. Давайте перепишем первоначальное содержимое catchphraseBuf перед тем, как продолжить:

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

Функция copy() имеет несколько дополнительных параметров, позволяющих нам настраивать данные, копируемые в другой буфер. Вот список всех параметров этой функции:

  • target — единственный необходимый параметр функции copy(). Как мы увидели ранее, это тот буфер, в который мы хотим копировать данные.
  • targetStart — индекс байтов в целевом буфере, в который мы собираемся копировать данные. По умолчанию значение равно 0, это означает, что копируются данные, начинающиеся с начала буфера.
  • sourceStart — индекс байтов в исходном буфере, откуда мы собираемся копировать данные.
  • sourceEnd — индекс байтов в исходном буфере, где мы должны прекратить копировать данные. По умолчанию значение равно длине буфера.

Поэтому для копирования Nananana из wordsBuf в catchphraseBuf наша цель должна быть catchphraseBuf, как и ранее. targetStart будет равен 0, поскольку мы хотим, чтобы слово Nananana стояло в начале catchphraseBuf. sourceStart должен быть равен 7, т.к. это индекс того, где слово Nananana начинается в wordsBuf. sourceEnd будет по-прежнему равен длине буферов.

В диалоге REPL скопируйте содержимое wordsBuf следующим образом:

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

REPL подтверждает, что было записано 8 байтов. Обратите внимание, что wordsBuf.length используется в качестве значения для параметра sourceEnd. Как и массивы, свойство length дает нам размер буфера.

Теперь посмотрим содержимое catchphraseBuf:

  • catchphraseBuf.toString();

REPL выдает следующее:

Output
'Nananana Turtle!'

Успешно! Мы смогли изменить данные catchphraseBuf, скопировав содержимое wordsBuf.

Если хотите, можете выйти из Node.js REPL. Отметьте, что при этом все созданные переменные станут недоступными:

  • .exit

Заключение

В этом руководстве вы узнали, что буферы представляют собой пространства фиксированной длины в памяти, хранящие бинарные данные. Сначала вы создали буферы, задав их размер в памяти и инициализировав их с существующими данными. Затем вы прочли данные из буфера посредством просмотра их отдельных байтов и при помощи методов toString() и toJSON(). В конце вы изменили данные, сохраненные в буфере, изменив их отдельные байты, а также используя методы write() и copy().

Буферы дают вам хорошее представление о том, как Node.js манипулирует бинарными данными. Теперь, когда вы можете взаимодействовать с буферами, вы можете наблюдать различные способы кодирования символов, влияющие на то, как хранятся данные. Например, вы можете создать буферы из строковых данных, которые не кодируются в UTF-8 или ASCII, и посмотреть их разницу в размере. Также вы можете взять буфер с UTF-8 и использовать toString() для его конвертации в другие системы кодирования.

Для получения информации о буферах в Node.js вы можете прочитать документацию Node.js в объекте Buffer. Если хотите продолжить изучение Node.js, то можете вернуться к серии Программирование на Node.js или ознакомиться с проектами и конфигурациями на нашей странице разделов Node.

0 Comments

Creative Commons License