Buffer: бинарные данные в Node
Buffer — это область памяти фиксированного размера для сырых байтов: так Node работает с файлами, сетью и любыми бинарными данными.
Buffer — встроенный в Node.js класс, представляющий неизменяемую по длине последовательность байтов (чисел 0–255). Это «строка из байтов», а не из символов: один элемент — это один байт, а не одна буква.
JavaScript в браузере почти не сталкивается с байтами: там есть строки, числа и объекты. Но Node.js живёт на сервере, где данные приходят и уходят в виде сырых байтов — содержимое файла, пакет из сокета, тело HTTP-запроса. Чтобы хранить и обрабатывать эти байты, и нужен Buffer. Под капотом это блок памяти вне обычной кучи V8, поэтому он быстр и не давит на сборщик мусора.
Этот урок — про то, зачем вообще отделять байты от строк, как одни и те же байты читаются в разных кодировках и где новички теряют данные, путая длину в символах с длиной в байтах.
Зачем это на практике
Buffer всплывает везде, где Node соприкасается с внешним миром:
- Чтение файла без указания кодировки:
fs.readFile('pic.png')вернётBuffer— потому что картинка это байты, а не текст. - Сетевые данные: каждый кусок (chunk) из TCP-сокета или HTTP-потока приходит как
Buffer. - Криптография и хеши:
cryptoотдаёт результат в виде байтов, которые потом кодируют вhexилиbase64. - Бинарные протоколы: разбор заголовков пакетов, где важен конкретный байт по конкретному смещению.
Примеры ниже используют Buffer и модуль fs — это серверный API Node, в браузере его нет. Поэтому кнопки «Запустить» под кодом нет: читайте его как образец и проверяйте в локальном Node.
Как создать Buffer
Современные способы создания — статические методы класса. Старый конструктор new Buffer(...) объявлен устаревшим и небезопасным (мог отдать «грязную» память), его использовать нельзя.
// из строки (по умолчанию кодировка utf-8)
const a = Buffer.from('Привет');
// из массива байтов напрямую
const b = Buffer.from([0x48, 0x69]); // байты 'H', 'i'
// пустой буфер на 8 байт, заполненный нулями
const c = Buffer.alloc(8);
console.log(a.length, b.length, c.length);
Вывод:
12 2 2
Обратите внимание на первое число: строка «Привет» — это 6 символов, но 12 байтов. Каждая кириллическая буква в UTF-8 занимает 2 байта. Вот первое важное различие: Buffer.length — это длина в байтах, а не в символах.
Зачем нужны два метода выделения памяти:
| Метод | Что делает |
Buffer.alloc(n) | создаёт буфер на n байтов, заполненный нулями — безопасно |
Buffer.allocUnsafe(n) | быстрее, но память НЕ обнулена (может содержать старые данные) |
Buffer.from(...) | создаёт буфер из строки, массива байтов или другого буфера |
По умолчанию берите Buffer.alloc и Buffer.from. allocUnsafe оправдан, только когда вы тут же полностью перезаписываете весь буфер и важна скорость.
Кодировки: одни байты — разный текст
Кодировка — это правило, как превратить байты в текст и обратно. Сами байты нейтральны; смысл им придаёт кодировка. Один и тот же Buffer можно «прочитать» по-разному методом toString(encoding).
const buf = Buffer.from('Hi');
console.log(buf.toString('utf-8')); // текст
console.log(buf.toString('hex')); // шестнадцатеричные байты
console.log(buf.toString('base64')); // base64
Вывод:
Hi 4869 SGk=
Здесь байты 0x48 0x69 — это «Hi» в UTF-8, «4869» в hex и «SGk=» в base64. Сами байты не менялись — менялся способ их показать. Основные кодировки в Node:
utf-8 | текст по умолчанию; кириллица и эмодзи занимают несколько байтов |
ascii / latin1 | один байт на символ; не годятся для кириллицы |
hex | каждый байт как две шестнадцатеричные цифры — удобно для отладки и хешей |
base64 | упаковка байтов в безопасные для текста символы (для JSON, ссылок, заголовков) |
Симметрично работает обратное направление: Buffer.from(str, 'base64') декодирует base64-строку обратно в байты. Это типичный приём, когда бинарные данные нужно передать там, где разрешён только текст.
Чтение и запись отдельных байтов
К буферу можно обращаться как к массиву по индексу — это даёт числовое значение байта (0–255). Для многобайтовых чисел есть методы readUInt16BE, writeUInt32LE и подобные — они нужны при разборе бинарных форматов, где число занимает 2 или 4 байта.
const buf = Buffer.from('ABC');
console.log(buf[0]); // 65 — код 'A'
buf[0] = 90; // меняем первый байт на код 'Z'
console.log(buf.toString()); // 'ZBC'
console.log(buf[3]); // undefined — выхода за границу нет
Вывод:
65 ZBC undefined
Содержимое буфера менять можно (байты перезаписываются), но длину — нельзя: буфер выделен под фиксированный размер. Чтобы «увеличить» данные, создают новый буфер, обычно через Buffer.concat([a, b]), который склеивает несколько буферов в один.
Buffer против строки
Когда что использовать? Если данные — это текст в известной кодировке, удобнее строка. Если это сырые байты (картинка, архив, кусок сетевого потока) или важен каждый байт — нужен Buffer. Главная ловушка возникает на границе: текст из нескольких байтов на символ нельзя резать по произвольному байту.
const buf = Buffer.from('ёж'); // 'ё' и 'ж' по 2 байта = 4 байта
// режем по 2 байта — случайно попали в середину символа
console.log(buf.slice(0, 1).toString()); // битый символ (�)
console.log(buf.slice(0, 2).toString()); // 'ё' — попали ровно
Срез по байтам 0..1 отрезал половину буквы «ё», и UTF-8-декодер не смог её восстановить — получился знак замены. А срез 0..2 совпал с границей символа и дал целую букву. Это объясняет, почему в потоках текст нельзя декодировать кусками наугад (об этом — в уроке про потоки).
Как это работает под капотом
Buffer — это представление над ArrayBuffer: блок «сырой» памяти, выделенный вне кучи V8. Это сделано осознанно. Сетевые и файловые данные приходят мегабайтами; если бы каждый байт был объектом JavaScript, сборщик мусора захлебнулся бы. Память буфера живёт отдельно, поэтому выделение и копирование быстрые.
Для эффективности маленькие буферы (по умолчанию до 4 КБ) Node нарезает из общего внутреннего пула. Практическое следствие: buf.length — это размер видимого «окна», а под ним может лежать буфер побольше. Поэтому buf.slice() (в новых версиях — buf.subarray()) не копирует данные, а возвращает вид на ту же память: изменишь срез — изменится и оригинал. Когда нужна именно независимая копия, используют Buffer.from(buf) или Buffer.copyBytesFrom.
Частые ошибки
- Путают длину в символах и в байтах.
'Привет'.lengthравно 6, ноBuffer.from('Привет').lengthравно 12. Для размера в байтах естьBuffer.byteLength(str). - Режут многобайтовый текст по байтам. Срез посреди UTF-8-символа даёт «битые» знаки. Текст режьте по символам или используйте
StringDecoder, который копит «хвосты». - Используют new Buffer(). Старый конструктор устарел и небезопасен. Только
Buffer.from,Buffer.alloc,Buffer.allocUnsafe. - Забыли кодировку при чтении файла.
fs.readFile(path)отдаётBuffer; чтобы получить строку, передайте кодировку:fs.readFile(path, 'utf-8'). - Считают, что slice копирует.
slice/subarrayделят память с оригиналом; для копии нужен явныйBuffer.from(buf).
Итоги
Buffer— фиксированная по длине последовательность байтов (0–255) для бинарных данных: файлы, сеть, крипто.- Создавайте через
Buffer.from(из строки/массива) иBuffer.alloc(нули);new Bufferзапрещён. - Кодировка задаёт смысл байтов:
toString('utf-8'|'hex'|'base64')показывает одни и те же байты по-разному. - Длина буфера — в байтах, а не в символах; кириллица в UTF-8 — по 2 байта на букву.
- Содержимое менять можно, длину — нет;
slice/subarrayделят память, для копии нуженBuffer.from(buf).