Buffer в Node.js: работа с бинарными данными

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов с бейджем. Код дан по документации Node.js. Говорим прямо, а не ставим бейдж «проверено».

Buffer — самый «нодовый» объект: в браузере его нет, а в Node он повсюду, где данные не текст. Разберём, зачем он и где расставлены грабли.

Зачем нужен Buffer

JavaScript-строки отлично хранят текст, но плохо — сырые байты. Картинка, аудио, зашифрованные данные, сетевой пакет — это последовательности байтов, которые не являются валидным текстом ни в какой кодировке. Попытка держать их в строке портит данные.

Buffer — это участок памяти фиксированного размера для сырых байтов. Его отдают все бинарные API Node: чтение файла без кодировки, сетевой сокет, crypto, zlib.

import { readFile } from 'node:fs/promises';

const bytes = await readFile('image.png');   // Buffer — сырые байты картинки
const text = await readFile('config.txt', 'utf8');   // строка — потому что указали кодировку

Создание буфера

Buffer.from('привет', 'utf8');     // из строки
Buffer.from([0x68, 0x69]);         // из массива байтов
Buffer.alloc(10);                  // 10 обнулённых байтов
Buffer.allocUnsafe(10);            // 10 байтов БЕЗ обнуления

Здесь живёт единственная опасная развилка — alloc против allocUnsafe.

alloc против allocUnsafe

Buffer.alloc(n)

Выбор по умолчанию

  • Обнуляет память перед выдачей
  • Предсказуемое содержимое
  • Чуть медленнее
  • Безопасно отдавать наружу

Buffer.allocUnsafe(n)

Только с полной перезаписью

  • Не обнуляет — там старые данные процесса
  • Быстрее
  • Отдать не заполнив = утечка чужой памяти
  • Оправдан, только если сразу пишете весь буфер

allocUnsafe быстрее, потому что пропускает обнуление — но в выданной памяти лежат остатки предыдущих данных процесса: обрывки паролей, чужих запросов, чего угодно. Отдать такой буфер клиенту, не перезаписав целиком, — значит отправить ему случайные фрагменты чужой памяти. Пока не упрётесь в производительность, берите alloc.

Кодировки: байты и текст

Буфер превращают в строку и обратно, указывая кодировку:

const buf = Buffer.from('привет', 'utf8');

buf.length;                // 12 — БАЙТ, не символов
buf.toString('utf8');      // "привет"
buf.toString('hex');       // "d0bfd180d0b8d0b2d0b5d182"
buf.toString('base64');    // "0L/RgNC40LLQtdGC"

Главная ловушка — длина. buf.length — это байты, а "привет".length — символы. Для кириллицы разница в два раза, для эмодзи — до четырёх. Отсюда классическая ошибка: считать размер текста по числу символов или наоборот.

Полезные кодировки: utf8 (текст), hex (для отладки и хешей), base64 (передача бинарного как текста, например в JSON или data-URL).

Склейка: только concat

// ❌ через строку — многобайтовый символ может порваться
let result = '';
for (const chunk of chunks) result += chunk;   // риск испортить кириллицу

// ✅ собираем байты, декодируем один раз
const result = Buffer.concat(chunks).toString('utf8');

Почему нельзя через строку: если кириллическая буква (два байта) окажется на границе двух буферов, преобразование каждого куска в строку по отдельности разорвёт её пополам, и вы получите «кракозябру». Buffer.concat склеивает байты, а декодирование происходит один раз в конце, когда символы точно целы.

Это ровно причина, по которой тело HTTP-запроса собирают через concat, а не строкой.

Чтение чисел из буфера

Буфер умеет то, чего не умеет обычный массив, — читать числа разного размера:

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

buf.readUInt8(0);        // 0   — один байт
buf.readUInt16BE(0);     // 1   — два байта, big-endian
buf.readUInt32LE(0);     // 256 — четыре байта, little-endian

BE и LE — порядок байтов (big-endian / little-endian). Это нужно при разборе бинарных форматов и сетевых протоколов, где число занимает несколько байтов в определённом порядке. Для прикладного кода — редко, но когда встречается, без этого никак.

Buffer и производительность

Буфер работает вне обычной кучи V8 (в разделе external из process.memoryUsage()), поэтому большие буферы не давят на сборщик мусора так, как большие строки. Но у них есть предел — buffer.constants.MAX_LENGTH, и очень большие данные всё равно обрабатывают потоком, а не целиком в буфере.


Смежные темы: модуль fs — где буферы возникают; потоки — обработка бинарного по кускам; crypto — хеши возвращают буферы. Полный список — в справочнике по Node.js.

Частые вопросы

Зачем нужен Buffer, если есть строки?
Строки — для текста, Buffer — для сырых байтов: файлов, картинок, сетевых пакетов, шифрования. Многие бинарные данные нельзя корректно представить строкой, а Buffer хранит их как есть.
Чем Buffer.alloc отличается от Buffer.allocUnsafe?
alloc обнуляет память перед выдачей — безопасно. allocUnsafe быстрее, но не обнуляет: в буфере остаются старые данные процесса. Отдавать такой буфер наружу не заполнив — утечка чужой памяти. По умолчанию берите alloc.
Почему длина буфера не равна длине строки?
Потому что многие символы занимают несколько байтов. Кириллица в UTF-8 — по два байта на букву, эмодзи — до четырёх. Buffer.from('привет').length вернёт 12, а строка — 6 символов.
Buffer — это массив?
Он наследник Uint8Array, поэтому ведёт себя как массив байтов: индексы, length, перебор. Но у него есть методы, которых нет у обычного массива: toString с кодировкой, работа с числами разного размера.
Как склеить несколько буферов?
Buffer.concat([buf1, buf2]). Складывать через строку нельзя: многобайтовый символ может разорваться между буферами и испортиться. Собирайте байты, декодируйте один раз в конце.