Node.js Internals
Buffer: Работа с бинарными данными
Представь: ты пишешь сервер для онлайн-игры. Каждую секунду приходит тысяча TCP-пакетов - координаты игроков, действия, чат. Если хранить каждый пакет как JavaScript-строку в UTF-16, сервер сожрёт 10GB RAM и будет тормозить. Buffer решает это: сырые байты, минимум копирований, нативная скорость.
- **Криптография**: `crypto.createHash('sha256').update(Buffer.from(password))` - хеширование работает с байтами, не строками. Если передать строку напрямую, кодировка может сломать безопасность
- **WebSocket**: при получении бинарного фрейма (например, protobuf) данные приходят как Buffer. Парсить их через String → JSON невозможно - это не текст, а закодированная структура
- **Изображения**: библиотека `sharp` (обработка изображений) работает с Buffer. Чтение PNG → изменение размера → сохранение в JPEG - всё через Buffer, без промежуточных файлов на диске
Зачем нужен Buffer
JavaScript создавался для браузеров, где основная единица данных - это текст (строки). Но в Node.js мы работаем с **сетью, файловой системой, криптографией** - там данные передаются как **сырые байты**, а не символы Unicode.
**Проблема**: JavaScript хранит строки в кодировке **UTF-16** (2 байта на символ). Для работы с TCP-пакетами, бинарными протоколами или изображениями это неэффективно и неудобно.
**Buffer** - это класс Node.js для работы с последовательностью байтов напрямую в памяти. Это как `uint8_t[]` в C или `byte[]` в Java, но с удобным API.
**Где это критично:** - **Криптография**: шифрование работает с байтами, не символами - **Сеть**: HTTP/2, WebSocket, TCP пакеты - это байты - **Файлы**: изображения, видео, PDF - не текст - **Производительность**: парсинг JSON из Buffer в 2-3 раза быстрее, чем из строки
Почему для работы с TCP-пакетами нельзя использовать обычные JavaScript строки?
Создание Buffer
В Node.js есть три основных способа создать Buffer. Каждый - для своей задачи, и у каждого свои подводные камни (особенно с безопасностью).
**Главное правило**: никогда не используй `new Buffer()` (deprecated с Nod 6). Используй статические методы: `Buffer.from()`, `Buffer.alloc()`, `Buffer.allocUnsafe()`.
На диаграмме видно: `allocUnsafe` **пропускает фазу Initialize** - это быстрее, но опасно (содержит случайные данные из памяти).
**Безопасность**: В 2016 году найдена уязвимость в пакете `ws` (WebSocket), где `allocUnsafe` утекал приватные данные клиентов. **Правило**: используй `allocUnsafe` только если точно знаешь, что сразу перезапишешь все байты.
Почему Buffer.allocUnsafe() может быть опасен для безопасности?
Кодировки (Encoding)
Buffer хранит байты, но часто нужно преобразовать их в текст (или наоборот). Для этого используются **кодировки** - правила, как байты превращаются в символы.
**Важно**: одни и те же байты могут быть прочитаны по-разному в зависимости от кодировки. `0x48` в ASCII - это 'H', а в UTF-16LE - половина символа.
**UTF-8** - стандарт для текста. **Base64** - для передачи байтов через JSON/HTTP. **Hex** - для debug и хешей (SHA256 → 64 hex символа).
**Реальный пример: JWT токен**. JWT состоит из 3 частей, разделённых точкой, каждая - это Base64:
**Производительность**: `toString()` с кодировкой - это нативная C++ функция в V8, очень быстрая. Но если преобразуешь Buffer → String → Buffer несколько раз в горячем пути, лучше работать с Buffer напрямую.
Почему JWT токены используют Base64 кодировку, а не просто UTF-8?
Операции с Buffer
Buffer - это не просто массив байтов. Это набор методов для эффективной работы с бинарными данными: копирование, сравнение, поиск, конкатенация.
**Главное отличие от массивов**: `.slice()` в Buffer возвращает **view** (ссылку на ту же память), а не копию! С Node 18+ используй `.subarray()` - это более явное название.
Shared Memory в subarray()
```typescript const buf = Buffer.from('Hello!'); // [0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21] const view = buf.subarray(2 6); // [0x6c, 0x6c, 0x6f, 0x21] - 'llo!' view[0] = 0x4A; // Меняем 'l' → 'J' console.log(buf.toString()); // "HeJlo!" - оригинал изменился! ``` **Почему?** `subarray()` возвращает ссылку на ту же память. Изменения в `view` затрагивают `buf`.
**Ключевой момент**: когда изменяешь subarray, меняется и **оригинальный Buffer** (shared memory). Это эффективно (нет копирования), но опасно!
**Реальный пример: парсинг HTTP chunked transfer encoding**. HTTP может отправлять данные кусками, каждый начинается с размера в hex:
**Производительность**: `Buffer.concat()` создаёт новый Buffer и копирует данные. Если конкатенируешь тысячи мелких кусков (например, стриминг), лучше собирать в массив и сделать один `concat()` в конце.
Что произойдёт с оригинальным Buffer при изменении view, полученного через .subarray()?
Связь с TypedArrays
Buffer - это не уникальная фича Node.js. Это **обёртка над Uint8Array** из стандарта JavaScript (ES6). Под капотом Buffer использует те же механизмы, что и TypedArrays в браузерах.
**TypedArray** - это семейство классов для работы с бинарными данными: Uint8Array, Int16Array, Float32Array и т.д. Все они построены над **ArrayBuffer** - блоком памяти.
На диаграмме видно: **ArrayBuffer** - это фундамент. Все остальные классы - это **views** (интерпретации) той же памяти.
**Реальный пример: парсинг бинарных данных (PNG header)**. PNG файл начинается с сигнатуры + метаданных в разных форматах:
**DataView**: ещё один способ читать ArrayBuffer с явным контролем endianness. Полезно для сетевых протоколов (network byte order = big-endian).
Что такое ArrayBuffer и чем он отличается от Buffer?
Управление памятью
Buffer работает с памятью вне V8 heap (в случае больших буферов) или использует **pooling** для маленьких. Это критично для производительности, но создаёт особенности при работе с GC.
**Buffer pooling**: Node.js выделяет большие блоки памяти (8KB) и нарезает их на маленькие Buffer. Это снижает фрагментацию и ускоряет аллокацию.
На диаграмме: маленькие Buffer (<4KB) берутся из **пула**, большие (>512KB) выделяются через malloc() **вне V8 heap**, чтобы не раздувать GC.
**Безопасность и pooling**: когда используешь `allocUnsafe()`, можешь получить кусок пула, где раньше были **чужие данные**. Это может утечь конфиденциальную информацию!
**Реальный пример: утечка памяти при стриминге**. Если сохраняешь ссылки на маленькие Buffer из большого потока, можешь случайно удержать весь поток в памяти!
**Производительность**: если обрабатываешь много данных (например, video streaming), используй **Buffer pool** вручную - выделяй большие буферы и переиспользуй их.
Buffer.alloc() и Buffer.allocUnsafe() - это одно и то же, просто разные названия
Buffer.alloc() заполняет память нулями (безопасно, но медленнее). Buffer.allocUnsafe() возвращает неинициализированную память (быстро, но может содержать старые данные - риск утечки)
allocUnsafe() пропускает этап обнуления памяти (zeroing). Если сразу не перезаписать все байты, можно прочитать остатки старых данных (пароли, токены). Используй alloc() по умолчанию, allocUnsafe() - только когда точно знаешь, что заполнишь Buffer полностью.
Почему Buffer.allocUnsafe() может привести к утечке конфиденциальных данных?
Ключевые идеи
- **Buffer - это Uint8Array с доп. методами**: работает с сырыми байтами (1 байт = 8 бит), в отличие от String (UTF-16, 2 байта на символ). Построен над ArrayBuffer из ES6
- **Безопасность**: `Buffer.alloc()` безопасен (заполняет нулями), `Buffer.allocUnsafe()` может утечь старые данные из памяти. Используй `alloc()` по умолчанию, `allocUnsafe()` - только если сразу перезапишешь все байты
- **Производительность**: `.subarray()` создаёт view (shared memory), не копию. `Buffer.concat()` в цикле - антипаттерн (квадратичная сложность). Для больших данных (>512KB) Node.js выделяет память вне V8 heap
- **Кодировки**: UTF-8 для текста, Base64 для передачи байтов через JSON/HTTP, Hex для debug. Одни и те же байты читаются по-разному в зависимости от encoding
- **TypedArrays**: Buffer === Uint8Array + Node.js API. ArrayBuffer - низкоуровневый блок памяти, на котором строятся все views (Uint8Array, Int16Array, DataView)
Связанные темы
Buffer - фундамент для работы с I/O в Node.js. Вот как он связан с другими темами:
- Streams — Streams передают данные кусками (chunks) - это Buffer. Методы `stream.read()`, `stream.write()` работают с Buffer, не строками
- Crypto — Все криптографические операции (hash, encrypt, sign) требуют Buffer. Кодировка строки влияет на результат хеша
- File System — fs.readFile() возвращает Buffer (по умолчанию). Для текста нужно явно указать encoding: 'utf8'
Вопросы для размышления
- Когда стоит использовать Buffer.allocUnsafe() вместо Buffer.alloc()? В каких ситуациях выигрыш в скорости оправдывает риск утечки данных?
- Почему JWT токены используют Base64URL, а не просто Base64? Подсказка: что произойдёт с символами '+' и '/' в URL?
- Как бы ты реализовал парсинг multipart/form-data (загрузка файлов через HTTP)? Как найти границы (boundaries) между частями, используя Buffer.indexOf()?