Использование буферов в 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(), данные, возвращаемые в обратный вызов или обещание, представляют собой объект буфера . Кроме того, когда в Node.js выполняются HTTP-запросы, они возвращают потоки данных, которые временно сохраняются во внутреннем буфере, когда клиент не может обработать поток сразу целиком.

Буферы полезны, когда вы работаете с двоичными данными, обычно на более низких уровнях сети. Они также дают вам возможность выполнять манипуляции с данными с тонкой настройкой в 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 (Read-Evaluate-Print-Loop). Если вам нужно освежить в памяти, как эффективно использовать Node.js REPL, вы можете прочитать наше руководство по Как использовать Node.js REPL.
  • Для этой статьи мы ожидаем, что пользователь будет уверенно владеть основами JavaScript и его типами данных. Вы можете изучить эти основы с помощью нашей серии Как писать на JavaScript.

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

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

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

Давайте откроем REPL Node.js, чтобы убедиться в этом сами. В вашем терминале введите команду node:

  1. node

Вы увидите приглашение, начинающееся с >.

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

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

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

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

В вашем терминале создайте новый буфер при приглашении REPL, заполненный 1ми:

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

Мы только что создали новый объект буфера, который ссылается на область в памяти, содержащую 1 КБ 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, используя ASCII-представление.

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

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

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

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

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

  • Массива целых чисел: Целочисленные значения могут быть между 0 и 255.
  • ArrayBuffer: Это объект JavaScript, который хранит фиксированную длину байтов.
  • A string.
  • Еще одного буфера.
  • Другие объекты JavaScript, которые имеют свойство Symbol.toPrimitive. Это свойство указывает JavaScript, как преобразовать объект в примитивный тип данных: boolean, null, undefined, number, string или symbol. Вы можете прочитать больше о символах в JavaScript на документации Mozilla: documentation.

Давайте посмотрим, как мы можем создать буфер из строки. Введите это в приглашение 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 соответствует UTF-8 представлению буквы H.

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

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

  1. hiBuf[1];

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

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

  1. hiBuf[2];

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

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

  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'

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

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

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

  1. hiBuf.toString('hex');

Это выражение приведет к:

Output
'486921'

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

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

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

  1. hiBuf.toJSON();

REPL вернет:

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

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

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

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

  1. tenZeroes.toJSON();

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

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

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

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

Шаг 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 введите следующую команду:

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

Метод write() вернул 3 в REPL. Это потому, что он записал три байта данных. Каждая буква имеет размер одного байта, так как этот буфер использует кодировку 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'

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

В отличие от этого, давайте создадим Буфер, который содержит четыре байта:

  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!');

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

Чтобы скопировать данные из одного буфера в другой, мы будем использовать метод 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() имеет еще несколько параметров, которые позволяют настраивать, какие данные копируются в другой буфер. Вот список всех параметров этой функции:

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

Итак, чтобы скопировать Nananana из wordsBuf в catchphraseBuf, наш target должен быть catchphraseBuf, как и раньше. targetStart должен быть 0, так как мы хотим, чтобы Nananana появился в начале catchphraseBuf. sourceStart должен быть 7, так как это индекс, с которого начинается Nananana в wordsBuf. 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!'

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

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

  1. .exit

Заключение

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

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

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

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