Что такое Buffer в Node.js: байты и кодировки¶
Источник: theNodeBook — What Is a Buffer
Buffer в Node.js — это последовательность байтов фиксированного размера для бинарных данных. Файлы, TCP‑сокеты, TLS‑записи, хеши, сжатые payload и заголовки изображений попадают в прикладной код как байты ещё до того, как вы присвоите им текстовый или протокольный смысл. Практичный ответ прост: Buffer даёт JavaScript тип данных на уровне байтов для I/O. Он хранит значения от 0 до 255, поддерживает кодирование и декодирование и может передавать память в нативные API без прогона каждого payload через строки.
Что такое Buffer в Node.js¶
Buffer построен поверх Uint8Array с node‑специфичными методами выделения памяти, кодировок и I/O. fs.readFile() может вернуть Buffer. buf.toString() декодирует байты в текст. Если держать значение как Buffer, сохраняется точность байтов для сети, crypto, сжатия и файловых форматов.
На ближайшие главы лучше заварить кофе. Мы проведём время с одной из самых базовых и, честно говоря, самых непонятых частей Node.js. Если вы когда‑нибудь смотрели на <Buffer ...> в консоли и чувствовали лёгкое беспокойство — вы в нужном месте.
Можно процитировать то, что находится за пять минут в Google: «Buffer — это фиксированный кусок памяти, бла‑бла‑бла…» — и закончить. Но где в этом настоящее понимание? Речь не об изучении API, а об исправлении допущения, которое подталкивает JavaScript: всё можно трактовать как текст.
Освоение Buffer полезно не только в Node.js. Массивы байтов, кодировки и управление памятью универсальны в системном программировании. Эти знания дадут фору, если вы перейдёте на Go, Rust, C++ или Java.
Мир за пределами строки¶
Как JavaScript‑разработчики, мы обычно работаем со структурированными данными. Они приходят JSON, мы парсим объекты, манипулируем строками и снова отдаём JSON. Большая часть прикладных данных — текст, символы Unicode, человекочитаемая информация. Даже массивы чисел чаще всего — просто числа: количества, счёт, координаты. Отсюда и наши дефолтные ожидания.
Высокопроизводительному TCP‑прокси нужно другое: взять каждый байт с одного соединения и переслать на другое без изменений. Без разбора. Без правок. Только передача. Сервис обработки изображений при чтении первых 512 байт загруженного JPEG для EXIF имеет такое же требование на уровне байтов.
Вы киваете, открываете редактор — и замираете.
Как представить поток сырых данных изображения? TCP‑пакеты? Какой тип данных JavaScript для этого подходит?
Первой мыслью опытного JS‑разработчика будет строка. Это единственный примитив для последовательности «чего‑то». На первый взгляд кажется рабочим.
Здесь и проявляется несоответствие. Это не «недостающая фича» JavaScript, а глубокий разрыв между дизайном языка и задачей. JavaScript родился в браузере. Раннюю модель runtime формировали DOM, события и AJAX — API вокруг HTML, CSS и текстовых форматов.
Node.js перенёс JavaScript в серверный I/O. Файловые системы, сокеты, криптография и низкоуровневые вызовы ОС оперируют байтами. Сырыми, неинтерпретированными байтами.
Эта глава про этот конфликт. Мы разберём, почему штатные средства JavaScript не просто неэффективны, а опасны для бинарных данных. Мы не только выучим API Buffer, но и построим ментальную модель, зачем Buffer вообще пришлось изобретать. Увидим проблему на практике, архитектуру памяти, которая делает решение возможным, и то, как проприетарное решение Node слилось со стандартами современного JavaScript.
Прежде чем смотреть, как строки ломаются, нужно ясно понять, на чём они ломаются. Слово «байт» уже встречалось — что оно значит? Короткий байтовый экскурс поможет.
Биты и байты¶
Всё в современном компьютере — от текста до 3D‑игры — сводится к бинарному состоянию: вкл или выкл. «Выкл» — 0, «вкл» — 1. Эта единица — бит, наименьшая единица данных.
1 2 | |
Одного бита мало: да/нет, true/false. Для полезной работы биты группируют. По давней конвенции — по восемь в группе.
Группа из 8 бит — байт.
1 2 | |
Это базовый кирпич. «Бинарные данные» — последовательность таких байтов. Файл в 1 МБ — около миллиона 8‑битных паттернов.
Главное: байт сам по себе не несёт смысла. Это просто паттерн. Байт 01001001 не «буква I», не «синий цвет» и не нота — пока вы не примените интерпретацию. Отсюда и будущие проблемы.
Байт как число¶
Данные — последовательность чисел. Смысл задаёт ваш код интерпретацией («это UTF‑8 символ» или «интенсивность синего пикселя»). Большинство багов с бинарными данными — неверная интерпретация.
Прямая интерпретация байта — число. Как получить число из 1 и 0? Двоичная (base‑2) система: позиции — степени двойки.
Структура байта, чтение справа налево:
1 2 3 4 5 6 7 | |
Складываем значения позиций с 1. Пример: 01001001.
1 2 3 4 5 6 7 8 | |
Паттерн 01001001 — целое 73.
С 8 битами минимум 00000000 (0), максимум 11111111 (255). Один байт — любое целое от 0 до 255. Отсюда частые упоминания 255 в низкоуровневом коде (например, RGB).
Диапазон 0–255 важен. При доступе к байту по индексу buf[i] значения всегда в этом диапазоне.
Байт как символ¶
Что если договориться о таблице: 65 → 'A', 66 → 'B'?
Так устроен ASCII — схема интерпретации, контракт.
1 2 3 4 5 6 7 8 9 10 11 12 | |
По ASCII байт 01001001 (73) — заглавная 'I'.
Биты не менялись. Число не менялось. Меняется только интерпретация. Кодировка символов — правила сопоставления чисел и символов. ASCII простая; UTF‑8 сложнее и покрывает символы почти всех языков.
Интерпретация 3: байт как что угодно ещё
Тот же 01001001 (73) может означать:
- в изображении — интенсивность синего канала пикселя;
- в аудио — сэмпл волны;
- в сети — часть IP‑адреса;
- в программе — машинную инструкцию.
Компьютеру всё равно — он видит 01001001. Контекст и интерпретацию даёт ваш код. «Катастрофа со строками» — следствие неверной интерпретации.
Масштаб: последовательности и сокращения
Редко работают с одним байтом — с тысячами и миллионами. В двоичном виде это нечитаемо:
1 2 3 | |
Удобнее шестнадцатеричная (base‑16) система: 0–9 и A–F. Одна hex‑цифра — ровно 4 бита (ниббл), два hex‑символа — весь байт.
1 2 3 4 5 6 7 8 9 | |
«HELLO»:
01001000→4801000101→4501001100→4C(дважды)01001111→4F
В hex: 48 45 4C 4C 4F.
Именно это вы видите в console.log для Buffer: <Buffer 48 45 4c 4c 4f>. Это не другой формат — другой способ показать те же байты.
Числа больше 255 требуют нескольких байтов: 16‑битное — до 65 535, 32‑битное — до ~4,2 млрд. Вопрос порядка: для байтов 0x12 и 0x34 в памяти это [0x12][0x34] или [0x34][0x12]?
Это порядок байтов (endianness). Big‑Endian — старший байт первым ([0x12][0x34]). Little‑Endian — младший первым ([0x34][0x12]). В сети часто Big‑Endian («network byte order»); многие CPU x86 — Little‑Endian. Отсюда buf.readInt16BE() и buf.readInt16LE() — вы явно задаёте порядок.
Endianness — классическая ловушка при данных из сети и с диска. Если многобайтовое число «не сходится», проверьте порядок байтов.
Трюк «A–C–F» для запоминания hex‑букв¶
Не нужно зубрить все шесть букв сразу. Достаточно трёх опор: A, C и F.
- A = 10 — первая буква после 9.
- C = 12 — на две позиции после A.
- F = 15 — последняя однозначная hex‑цифра.
Остальное — между опорами:
- B = 11 — между A и C (или
A + 1). - D = 13 — после C.
- E = 14 — перед F.
| Опорные значения | Как запомнить | «Между» |
|---|---|---|
| A = 10 | Первая буква после 9 | |
| B = 11 | Между A и C | |
| C = 12 | Две позиции после A | |
| D = 13 | После C | |
| E = 14 | Перед F | |
| F = 15 | Последняя hex‑цифра |
Как читать hex‑байт¶
B7 или 4E — число в системе с основанием 16. Байт всегда две hex‑цифры, у каждой — разряд.
Два разряда¶
Справа — единицы (16⁰, ×1). Слева — шестнадцатки (16¹, ×16).
| Разряд «шестнадцатков» (×16) | Разряд «единиц» (×1) |
|---|---|
| Левая цифра | Правая цифра |
Пример: A9 → десятичное.
- Цифры:
Aслева,9справа. - Слева:
A = 10,10 × 16 = 160. - Справа:
9 × 1 = 9. - Сумма:
160 + 9 = **169**.
С практикой перевод ускоряется до секунд.
Ещё пример: C5¶
Cи5.C = 12,12 × 16 = 192.5 × 1 = 5.192 + 5 = **197**.
Зачем hex? Байт — 8 бит; одна hex‑цифра — 4 бита, две — весь байт. C5 читать проще, чем 11000101.
Связь с Node.js¶
При fs.readFileSync('logo.png') ОС отдаёт сырую последовательность байтов — 01001001, 11101010 и т.д. По спецификации PNG это цвета пикселей, размеры, метаданные сжатия, а не текст ASCII/UTF‑8.
Проблема впереди — когда JavaScript применяет к PNG текстовые правила UTF‑8. Данные портятся.
С этой базой посмотрим, как всё ломается на практике.
Демо: почему текст не подходит¶
Допустим, в проекте есть logo.png — бинарный файл. Задача: прочитать в память и записать в logo-copy.png. Простое копирование.
Наивный вариант с fs выглядит разумно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
Нужен logo.png в той же директории. Вывод может быть таким:
1 2 3 4 5 6 7 8 | |
Первый сигнал — «каша» в выборке и ромбики с вопросом: символ замены (U+FFFD). Это не случайный мусор, а официальный знак Unicode — разберём ниже.
Главное доказательство — файловая система. Появится logo-corrupted.png. Он почти наверняка меньше оригинала и не откроется в просмотрщике. Мы не скопировали данные — испортили их.
Это не баг Node.js, а неверное понимание запроса.
Что такое строка в JavaScript? Не массив байтов. Строка — неизменяемая последовательность символов. V8 обычно хранит их в UTF‑16. Строка — слой абстракции: не сырые байты, а интерпретация по правилам Unicode.
Центр проблемы — аргумент 'utf8' в readFileSync. Мы не просто читаем файл, а приказываем: «прочитай байты logo.png и декодируй их как валидную UTF‑8 последовательность».
Ловушка UTF‑8. PNG — структурированный бинарный формат: пиксели, сжатие, палитры, контрольные суммы. Первые четыре байта сигнатуры PNG в hex: 89 50 4E 47.
Декодер UTF‑8 на байте 0x89 спотыкается: значения > 0x7F (127) часто начинают многобайтовый символ, но 0x89 не может быть началом валидной UTF‑8 последовательности.
Корректный декодер не падает — он выдаёт U+FFFD, официальный символ замены (�). Именно его вы видели в консоли.
Появление символа замены (�) — красный флаг: произошло необратимое lossy‑преобразование. Исходные байты, которые декодер не понял, выброшены и заменены. Данные испорчены навсегда.
Декодер выбросил 0x89 и подставил три байта UTF‑8 для U+FFFD (EF BF BD). Информация потеряна. Так происходит для каждого невалидного фрагмента. Отсюда другой размер logo-corrupted.png и мусор внутри — мы сохранили не файл, а обломки неудачного декодирования.
«А если 'latin1' или старый 'binary'?»
latin1 (ISO‑8859‑1) задаёт взаимно однозначное отображение байтов 0–255 на первые 256 code point Unicode. С 'latin1' round trip может сработать — файл совпадёт с оригиналом.
Проблема решена? Нет. Это опасный обман. Движок всё равно держит бинарные данные как строку; V8 может применять текстовые оптимизации. Семантически вы врёте runtime: пиксели объявлены «европейскими буквами». Отсюда тонкие баги при передаче в API, ожидающие настоящий текст.
'latin1' для «сохранения» бинарных данных в строке — хрупкий хак. Семантика неверна, поведение с другими API и будущими оптимизациями движка непредсказуемо. Правильно: не использовать строки для бинарных данных.
Дело не в выборе кодировки, а в самом декодировании. Нужны сырые байты без интерпретации как текст. В «чистом» JavaScript такого типа долго не было.
Зачем Node понадобилась своя память¶
Строки не подходят — нужна структура «массив байтов». Прежде чем про Buffer, важно где он живёт в памяти.
V8 управляет V8 heap — областью с продвинутым GC, заточенным под множество мелких связанных JS‑объектов со средним временем жизни.
Сервису обработки видео нужно не 512 байт, а 500 МБ файла в памяти.
Если бы «массив байтов» жил в V8 heap, при major GC движок мог бы сканировать и переносить весь полугигабайтный блок — «stop-the-world» на секунды, event loop встал.
Node принимает другое решение: Buffer выделяется вне управляемого V8 heap.
Это ключевая архитектурная идея: высокопроизводительная обработка бинарных данных в Node без паралича GC.
Как тогда работать из JavaScript?
Объект Buffer в коде — не сама плита памяти, а лёгкий handle на V8 heap с метаданными (длина) и указателем на сырой slab в RAM, которым управляет C++‑ядро Node.
Модель:
- Raw slab — большой непрерывный блок в RAM, байты файла или пакета.
- JS handle — маленький объект на V8 heap с адресом slab.
GC видит только handle: двигает и собирает его эффективно, не трогая 500 МБ slab. Когда handle собран, через weak references C++ освобождает slab и возвращает память ОС.
Удобство JS‑объекта + тяжёлое управление большими бинарными блоками вне V8.
Есть цена: выделение у ОС медленнее «bump‑pointer» в V8 heap; появляется иной класс рисков (утечки, границы). Подробнее — в следующих главах; сейчас важна двухкучная модель.
Много мелких Buffer может создаваться медленнее, чем мелкие JS‑объекты — overhead вызова C++. Node смягчает это пулом памяти (отдельная глава).
Buffer — решение Node¶
Проблема (строки опасны) и архитектура (память вне heap) ясны — переходим к инструменту.
«Почему не ArrayBuffer и Uint8Array?» В 2009 году, когда Ryan Dahl создавал Node, TypedArray ещё не были стабильным стандартом в V8. Сервер без бинарных данных бессмысленен — Buffer появился из необходимости.
Buffer — изменяемая последовательность байтов фиксированного размера, прямой доступ к slab; элементы — целые 0–255.
new Buffer() давно deprecated — API неоднозначен и небезопасен.
В старом коде и примерах встречается new Buffer(). В современном коде никогда не используйте его: разное поведение от аргументов, утечки неинициализированной памяти. Только Buffer.alloc() и Buffer.from().
Основные статические методы: Buffer.alloc() и Buffer.from().
Buffer.alloc(size) — «чистый» буфер нужного размера, заполненный нулями:
1 2 3 4 5 | |
Обнуление (zeroing) важно для безопасности: ОС может отдать память с остатками другого процесса (пароли, ключи). Buffer.alloc() перезаписывает нулями. Buffer.allocUnsafe() пропускает zeroing ради скорости — только если сразу перезапишете весь буфер.
Buffer.allocUnsafe() быстрее, но буфер может содержать старые чувствительные данные. Используйте только при гарантии полной перезаписи сразу после выделения.
Buffer.from(thing) создаёт буфер из существующих данных — в том числе решает исходную задачу со строками:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Buffer.from('hello world', 'utf8') — обратная операция к провалу с readFileSync(..., 'utf8'): мы кодируем текст в UTF‑8 байты, а не разрушаем бинарник декодированием.
Индексный доступ как у массива:
1 2 3 4 5 6 7 8 | |
У Buffer — слой хелперов под серверные задачи Node, которых нет у браузерных TypedArray.
buf.toString() — текстовое представление для логов и транспорта:
1 2 3 4 5 6 7 8 9 10 | |
Это безопасное представление, не разрушительное декодирование PNG в UTF‑8.
buf.write() вставляет строку в бинарную структуру — пример HTTP‑ответа:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Позже подробнее про многобайтовые числа: buf.readInt16BE(), buf.writeInt16BE() — BE = Big‑Endian. Нужны для JPEG, wire‑протоколов БД и т.д.
Богатый API сделал Buffer незаменимым годами; затем стандарт JavaScript догнал Node.
Сближение Buffer и TypedArray¶
«У Buffer фиксированная длина и доступ к байтам — как у Uint8Array.»
Верно. В современном Node.js: класс Buffer — подкласс стандартного Uint8Array.
Node.js Buffer — это Uint8Array. Его можно передать в любой API (Node или браузерную библиотеку), ожидающий Uint8Array, без конвертации. Вы получаете эргономику Node и совместимость с веб‑стандартом.
Раньше Buffer был отдельным типом. С ~Node.js v3 его перевели на наследование от Uint8Array.
1 2 3 4 5 | |
Любой API на Uint8Array принимает Buffer. Конвертация не нужна.
Зачем тогда имя Buffer и отдельные методы? Это подкласс: все методы Uint8Array (.slice(), .subarray(), .map()…) плюс .toString('hex'), .write(), .readInt16BE() и т.д.
И Buffer, и Uint8Array — представления (views) над ArrayBuffer:
ArrayBuffer— сырая плита памяти без прямого чтения/записи байтов; не знает, int это или float — только ресурс.TypedArrayиBuffer— «линзы» с API:Uint8Array— 8‑битные беззнаковые целые;Buffer— то же плюс node‑методы.
Buffer.alloc(10) под капотом: выделить ArrayBuffer на 10 байт (вне V8 heap), создать Buffer‑view и вернуть его.
Свойство .buffer у каждого Buffer:
1 2 3 4 5 6 7 8 9 | |
Несколько view на один ArrayBuffer:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Изменения через один view видны в другом — одна память, разные интерпретации. Полезно в высокопроизводительных библиотеках без лишних копий.
Несколько view на один ArrayBuffer — мощная оптимизация: разные представления одних данных (структура из int и float) без копирования.
Итог¶
Краткий путь главы:
- простая задача с бинарным файлом обнажила конфликт текстовых дефолтов JS и байтового systems programming;
- строка не просто медленна — она портит данные, навязывая лингвистическую интерпретацию;
- двухкучная модель держит большие бинарные блоки вне V8 GC;
Buffer— прагматичный server‑side инструмент с богатым API;- сегодня он интегрирован в экосистему как подкласс
Uint8ArrayнадArrayBuffer.
Одна фраза на запоминание:
Node.js Buffer — производительный, эргономичный для сервера подкласс Uint8Array, view над сырым блоком памяти вне garbage-collected heap V8.
Вы понимаете, зачем память вне heap, почему не строка и как это связано с браузерными стандартами.
Это основа почти любой высоконагруженной работы в Node. С моделью «куска бинарных данных в покое» можно переходить к Streams — данные идут не одним гигантским blob, а последовательностью Buffer. В следующей главе — как данные текут по приложению по частям; это ключ к масштабируемым и экономным по памяти системам.
Связанное чтение¶
- Предыдущая: Жизненный цикл процесса Node.js
- Далее: Выделение Buffer в Node.js: alloc, allocUnsafe, pooling