← JavaScript/ArrayBuffer и TypedArrays#157 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

ArrayBuffer и TypedArrays

Представь, что ты разрабатываешь мессенджер и отправляешь голосовые сообщения. Сервер возвращает не текст, а поток байтов — сжатый аудиофайл. WebSocket доставляет эти байты как ArrayBuffer. Тебе нужно прочитать заголовок, понять кодек, размер, канал — всё это делается через TypedArrays и DataView. Без этих инструментов бинарный протокол недоступен.

Какую проблему решает

JavaScript исторически работал только со строками и числами. Но сеть, файлы, WebGL, WebAssembly — всё это бинарные данные. ArrayBuffer даёт прямой доступ к байтам памяти.

На основе предыдущих уроков

  • Fetch API: response.arrayBuffer() возвращает ArrayBuffer с телом ответа
  • WebSocket: бинарный режим передачи данных
  • Типы данных: обычные Number не позволяют работать с отдельными байтами
  • Архитектура: буфер и представления

    ArrayBuffer — сырой блок памяти фиксированного размера. Читать байты напрямую нельзя — нужно создать представление (view):

    const buffer = new ArrayBuffer(16)  // 16 байт, все нули
    console.log(buffer.byteLength)      // 16

    TypedArray — «линза», через которую интерпретируются байты:

    | Тип | Байт/эл | Диапазон | Применение |

    |---|---|---|---|

    | Uint8Array | 1 | 0..255 | Сырые байты, PNG/JPEG |

    | Int16Array | 2 | -32768..32767 | Аудио-сэмплы |

    | Int32Array | 4 | ±2 млрд | Координаты, индексы |

    | Float32Array | 4 | float | WebGL вершины |

    | Float64Array | 8 | double | Точные вычисления |

    const buffer = new ArrayBuffer(8)
    const bytes  = new Uint8Array(buffer)   // 8 элементов по 1 байту
    const ints   = new Int32Array(buffer)   // 2 элемента по 4 байта
    
    // Оба смотрят на ОДИН И ТОТ ЖЕ буфер
    bytes[0] = 255
    bytes[1] = 0
    console.log(ints[0])  // -256 (те же байты, другая интерпретация)

    DataView — смешанные типы

    Когда структура содержит поля разных типов (как заголовок бинарного файла), DataView позволяет читать произвольные типы по смещению:

    const buf  = new ArrayBuffer(10)
    const view = new DataView(buf)
    
    view.setUint8(0, 0xFF)          // 1 байт по смещению 0
    view.setUint16(1, 1024, true)   // 2 байта, little-endian
    view.setFloat32(3, 3.14, true)  // 4 байта
    
    console.log(view.getUint8(0))              // 255
    console.log(view.getUint16(1, true))       // 1024
    console.log(view.getFloat32(3, true).toFixed(2))  // 3.14

    slice vs subarray

    const src = Uint8Array.from([1, 2, 3, 4, 5])
    
    const copy = src.slice(1, 4)     // КОПИЯ новых данных: [2, 3, 4]
    const view = src.subarray(1, 4)  // Тот же буфер, другие границы: [2, 3, 4]
    
    view[0] = 99  // Изменяет src!
    console.log(Array.from(src))   // [1, 99, 3, 4, 5]
    console.log(Array.from(copy))  // [2, 3, 4] — не изменился

    Типичные ошибки

    Ошибка 1: Попытка читать ArrayBuffer напрямую

    // НЕВЕРНО
    const buf = new ArrayBuffer(4)
    console.log(buf[0])  // undefined — у ArrayBuffer нет индексов!
    
    // ВЕРНО
    const view = new Uint8Array(buf)
    console.log(view[0])  // 0

    Ошибка 2: Несовпадение выравнивания

    // НЕВЕРНО — Int32Array требует смещение кратное 4
    const buf = new ArrayBuffer(8)
    const bad = new Int32Array(buf, 1)  // RangeError: byte offset is not aligned
    
    // ВЕРНО
    const good = new Int32Array(buf, 0)  // смещение 0 или 4

    Ошибка 3: Путаница big-endian / little-endian

    const buf  = new ArrayBuffer(2)
    const view = new DataView(buf)
    view.setUint16(0, 0x0102, true)   // little-endian: байты [02, 01]
    view.setUint16(0, 0x0102, false)  // big-endian:    байты [01, 02]
    // Сетевой порядок байтов — big-endian. x86 CPU — little-endian.

    В реальных проектах

  • WebSocket/бинарные протоколы: чтение заголовков кастомных форматов
  • Изображения: парсинг PNG/JPEG magic bytes для определения типа файла
  • WebGL: Float32Array для передачи вершин на GPU
  • WebAssembly: обмен данными через SharedArrayBuffer
  • Audio API: Int16Array для обработки PCM аудиосэмплов
  • Примеры

    Бинарный протокол: создание и парсинг заголовка сетевого пакета с DataView

    // ===== Сетевой пакет: бинарный протокол =====
    // Формат: [version: u8][type: u8][payloadLen: u32 LE][checksum: u16 LE]
    // Итого: 1 + 1 + 4 + 2 = 8 байт заголовка
    
    const PACKET_TYPES = { DATA: 0x01, ACK: 0x02, PING: 0x03, ERROR: 0xFF }
    
    function createPacketHeader(type, payloadLength) {
      const buf  = new ArrayBuffer(8)
      const view = new DataView(buf)
    
      view.setUint8(0, 1)              // version = 1
      view.setUint8(1, type)           // тип пакета
      view.setUint32(2, payloadLength, true)  // длина данных, LE
      // checksum — XOR всех предыдущих байт (упрощённый)
      const bytes = new Uint8Array(buf)
      let checksum = 0
      for (let i = 0; i < 6; i++) checksum ^= bytes[i]
      view.setUint16(6, checksum, true)
    
      return buf
    }
    
    function parsePacketHeader(buf) {
      const view  = new DataView(buf)
      const bytes = new Uint8Array(buf)
    
      const version    = view.getUint8(0)
      const type       = view.getUint8(1)
      const payloadLen = view.getUint32(2, true)
      const checksum   = view.getUint16(6, true)
    
      // Верифицируем checksum
      let expected = 0
      for (let i = 0; i < 6; i++) expected ^= bytes[i]
      const valid = checksum === expected
    
      return { version, type, payloadLen, checksum, valid }
    }
    
    console.log('=== Создание пакетов ===')
    const pingBuf  = createPacketHeader(PACKET_TYPES.PING, 0)
    const dataBuf  = createPacketHeader(PACKET_TYPES.DATA, 1024)
    
    const pingRaw = Array.from(new Uint8Array(pingBuf))
      .map(b => b.toString(16).padStart(2, '0')).join(' ')
    console.log('PING заголовок (hex):', pingRaw)
    // 01 03 00 00 00 00 02 00
    
    const pingInfo = parsePacketHeader(pingBuf)
    console.log('PING разобран:', pingInfo)
    // { version: 1, type: 3, payloadLen: 0, checksum: 2, valid: true }
    
    const dataInfo = parsePacketHeader(dataBuf)
    console.log('DATA разобран:', dataInfo)
    // { version: 1, type: 1, payloadLen: 1024, ..., valid: true }
    
    // ===== TypedArray — работа с аудио PCM =====
    console.log('\n=== Симуляция PCM аудио-буфера ===')
    
    // Аудио-сэмплы: 16-bit signed, 44100 Hz, 0.1 секунды = 4410 сэмплов
    const SAMPLE_RATE = 44100
    const DURATION_MS = 100
    const numSamples  = Math.floor(SAMPLE_RATE * DURATION_MS / 1000)
    
    const audioBuffer = new Int16Array(numSamples)
    
    // Генерируем синусоиду 440 Hz (нота A4)
    const FREQ = 440
    const AMPLITUDE = 32767 * 0.5  // 50% громкость
    for (let i = 0; i < numSamples; i++) {
      const t = i / SAMPLE_RATE
      audioBuffer[i] = Math.round(AMPLITUDE * Math.sin(2 * Math.PI * FREQ * t))
    }
    
    console.log('Сэмплов:', audioBuffer.length)
    console.log('Байт буфера:', audioBuffer.byteLength)
    console.log('Первые 5 сэмплов:', Array.from(audioBuffer.slice(0, 5)))
    // Значения ~ около 0 нарастают (синусоида начинается с 0)
    
    // Анализ: находим пик амплитуды
    let maxAmp = 0
    for (let i = 0; i < audioBuffer.length; i++) {
      if (Math.abs(audioBuffer[i]) > maxAmp) maxAmp = Math.abs(audioBuffer[i])
    }
    console.log('Пиковая амплитуда:', maxAmp)
    console.log('Пиковая амплитуда (%):', Math.round(maxAmp / 32767 * 100) + '%')
    
    // ===== slice vs subarray =====
    console.log('\n=== slice (копия) vs subarray (view) ===')
    
    const src  = Uint8Array.from([10, 20, 30, 40, 50])
    const copy = src.slice(1, 4)     // [20, 30, 40] — независимая копия
    const view = src.subarray(1, 4)  // [20, 30, 40] — тот же буфер
    
    view[0] = 99  // меняем через view
    console.log('src после view[0]=99:', Array.from(src))   // [10, 99, 30, 40, 50]
    console.log('copy — не изменился:',  Array.from(copy))  // [20, 30, 40]

    ArrayBuffer и TypedArrays

    Представь, что ты разрабатываешь мессенджер и отправляешь голосовые сообщения. Сервер возвращает не текст, а поток байтов — сжатый аудиофайл. WebSocket доставляет эти байты как ArrayBuffer. Тебе нужно прочитать заголовок, понять кодек, размер, канал — всё это делается через TypedArrays и DataView. Без этих инструментов бинарный протокол недоступен.

    Какую проблему решает

    JavaScript исторически работал только со строками и числами. Но сеть, файлы, WebGL, WebAssembly — всё это бинарные данные. ArrayBuffer даёт прямой доступ к байтам памяти.

    На основе предыдущих уроков

  • Fetch API: response.arrayBuffer() возвращает ArrayBuffer с телом ответа
  • WebSocket: бинарный режим передачи данных
  • Типы данных: обычные Number не позволяют работать с отдельными байтами
  • Архитектура: буфер и представления

    ArrayBuffer — сырой блок памяти фиксированного размера. Читать байты напрямую нельзя — нужно создать представление (view):

    const buffer = new ArrayBuffer(16)  // 16 байт, все нули
    console.log(buffer.byteLength)      // 16

    TypedArray — «линза», через которую интерпретируются байты:

    | Тип | Байт/эл | Диапазон | Применение |

    |---|---|---|---|

    | Uint8Array | 1 | 0..255 | Сырые байты, PNG/JPEG |

    | Int16Array | 2 | -32768..32767 | Аудио-сэмплы |

    | Int32Array | 4 | ±2 млрд | Координаты, индексы |

    | Float32Array | 4 | float | WebGL вершины |

    | Float64Array | 8 | double | Точные вычисления |

    const buffer = new ArrayBuffer(8)
    const bytes  = new Uint8Array(buffer)   // 8 элементов по 1 байту
    const ints   = new Int32Array(buffer)   // 2 элемента по 4 байта
    
    // Оба смотрят на ОДИН И ТОТ ЖЕ буфер
    bytes[0] = 255
    bytes[1] = 0
    console.log(ints[0])  // -256 (те же байты, другая интерпретация)

    DataView — смешанные типы

    Когда структура содержит поля разных типов (как заголовок бинарного файла), DataView позволяет читать произвольные типы по смещению:

    const buf  = new ArrayBuffer(10)
    const view = new DataView(buf)
    
    view.setUint8(0, 0xFF)          // 1 байт по смещению 0
    view.setUint16(1, 1024, true)   // 2 байта, little-endian
    view.setFloat32(3, 3.14, true)  // 4 байта
    
    console.log(view.getUint8(0))              // 255
    console.log(view.getUint16(1, true))       // 1024
    console.log(view.getFloat32(3, true).toFixed(2))  // 3.14

    slice vs subarray

    const src = Uint8Array.from([1, 2, 3, 4, 5])
    
    const copy = src.slice(1, 4)     // КОПИЯ новых данных: [2, 3, 4]
    const view = src.subarray(1, 4)  // Тот же буфер, другие границы: [2, 3, 4]
    
    view[0] = 99  // Изменяет src!
    console.log(Array.from(src))   // [1, 99, 3, 4, 5]
    console.log(Array.from(copy))  // [2, 3, 4] — не изменился

    Типичные ошибки

    Ошибка 1: Попытка читать ArrayBuffer напрямую

    // НЕВЕРНО
    const buf = new ArrayBuffer(4)
    console.log(buf[0])  // undefined — у ArrayBuffer нет индексов!
    
    // ВЕРНО
    const view = new Uint8Array(buf)
    console.log(view[0])  // 0

    Ошибка 2: Несовпадение выравнивания

    // НЕВЕРНО — Int32Array требует смещение кратное 4
    const buf = new ArrayBuffer(8)
    const bad = new Int32Array(buf, 1)  // RangeError: byte offset is not aligned
    
    // ВЕРНО
    const good = new Int32Array(buf, 0)  // смещение 0 или 4

    Ошибка 3: Путаница big-endian / little-endian

    const buf  = new ArrayBuffer(2)
    const view = new DataView(buf)
    view.setUint16(0, 0x0102, true)   // little-endian: байты [02, 01]
    view.setUint16(0, 0x0102, false)  // big-endian:    байты [01, 02]
    // Сетевой порядок байтов — big-endian. x86 CPU — little-endian.

    В реальных проектах

  • WebSocket/бинарные протоколы: чтение заголовков кастомных форматов
  • Изображения: парсинг PNG/JPEG magic bytes для определения типа файла
  • WebGL: Float32Array для передачи вершин на GPU
  • WebAssembly: обмен данными через SharedArrayBuffer
  • Audio API: Int16Array для обработки PCM аудиосэмплов
  • Примеры

    Бинарный протокол: создание и парсинг заголовка сетевого пакета с DataView

    // ===== Сетевой пакет: бинарный протокол =====
    // Формат: [version: u8][type: u8][payloadLen: u32 LE][checksum: u16 LE]
    // Итого: 1 + 1 + 4 + 2 = 8 байт заголовка
    
    const PACKET_TYPES = { DATA: 0x01, ACK: 0x02, PING: 0x03, ERROR: 0xFF }
    
    function createPacketHeader(type, payloadLength) {
      const buf  = new ArrayBuffer(8)
      const view = new DataView(buf)
    
      view.setUint8(0, 1)              // version = 1
      view.setUint8(1, type)           // тип пакета
      view.setUint32(2, payloadLength, true)  // длина данных, LE
      // checksum — XOR всех предыдущих байт (упрощённый)
      const bytes = new Uint8Array(buf)
      let checksum = 0
      for (let i = 0; i < 6; i++) checksum ^= bytes[i]
      view.setUint16(6, checksum, true)
    
      return buf
    }
    
    function parsePacketHeader(buf) {
      const view  = new DataView(buf)
      const bytes = new Uint8Array(buf)
    
      const version    = view.getUint8(0)
      const type       = view.getUint8(1)
      const payloadLen = view.getUint32(2, true)
      const checksum   = view.getUint16(6, true)
    
      // Верифицируем checksum
      let expected = 0
      for (let i = 0; i < 6; i++) expected ^= bytes[i]
      const valid = checksum === expected
    
      return { version, type, payloadLen, checksum, valid }
    }
    
    console.log('=== Создание пакетов ===')
    const pingBuf  = createPacketHeader(PACKET_TYPES.PING, 0)
    const dataBuf  = createPacketHeader(PACKET_TYPES.DATA, 1024)
    
    const pingRaw = Array.from(new Uint8Array(pingBuf))
      .map(b => b.toString(16).padStart(2, '0')).join(' ')
    console.log('PING заголовок (hex):', pingRaw)
    // 01 03 00 00 00 00 02 00
    
    const pingInfo = parsePacketHeader(pingBuf)
    console.log('PING разобран:', pingInfo)
    // { version: 1, type: 3, payloadLen: 0, checksum: 2, valid: true }
    
    const dataInfo = parsePacketHeader(dataBuf)
    console.log('DATA разобран:', dataInfo)
    // { version: 1, type: 1, payloadLen: 1024, ..., valid: true }
    
    // ===== TypedArray — работа с аудио PCM =====
    console.log('\n=== Симуляция PCM аудио-буфера ===')
    
    // Аудио-сэмплы: 16-bit signed, 44100 Hz, 0.1 секунды = 4410 сэмплов
    const SAMPLE_RATE = 44100
    const DURATION_MS = 100
    const numSamples  = Math.floor(SAMPLE_RATE * DURATION_MS / 1000)
    
    const audioBuffer = new Int16Array(numSamples)
    
    // Генерируем синусоиду 440 Hz (нота A4)
    const FREQ = 440
    const AMPLITUDE = 32767 * 0.5  // 50% громкость
    for (let i = 0; i < numSamples; i++) {
      const t = i / SAMPLE_RATE
      audioBuffer[i] = Math.round(AMPLITUDE * Math.sin(2 * Math.PI * FREQ * t))
    }
    
    console.log('Сэмплов:', audioBuffer.length)
    console.log('Байт буфера:', audioBuffer.byteLength)
    console.log('Первые 5 сэмплов:', Array.from(audioBuffer.slice(0, 5)))
    // Значения ~ около 0 нарастают (синусоида начинается с 0)
    
    // Анализ: находим пик амплитуды
    let maxAmp = 0
    for (let i = 0; i < audioBuffer.length; i++) {
      if (Math.abs(audioBuffer[i]) > maxAmp) maxAmp = Math.abs(audioBuffer[i])
    }
    console.log('Пиковая амплитуда:', maxAmp)
    console.log('Пиковая амплитуда (%):', Math.round(maxAmp / 32767 * 100) + '%')
    
    // ===== slice vs subarray =====
    console.log('\n=== slice (копия) vs subarray (view) ===')
    
    const src  = Uint8Array.from([10, 20, 30, 40, 50])
    const copy = src.slice(1, 4)     // [20, 30, 40] — независимая копия
    const view = src.subarray(1, 4)  // [20, 30, 40] — тот же буфер
    
    view[0] = 99  // меняем через view
    console.log('src после view[0]=99:', Array.from(src))   // [10, 99, 30, 40, 50]
    console.log('copy — не изменился:',  Array.from(copy))  // [20, 30, 40]

    Задание

    Ты разрабатываешь бинарный протокол для передачи координат GPS между устройствами. Каждая точка хранится в 8 байтах: широта (Float32, смещение 0) и долгота (Float32, смещение 4), оба — little-endian. Реализуй: - `encodeGPS(lat, lng)` — упаковывает координаты в ArrayBuffer из 8 байт - `decodeGPS(buf)` — читает координаты обратно из буфера - `encodeBatch(points)` — упаковывает массив точек `[{lat, lng}]` в один ArrayBuffer (8 байт × N)

    Подсказка

    encodeGPS: new ArrayBuffer(8), setFloat32(0, lat, true), setFloat32(4, lng, true). decodeGPS: getFloat32(0, true), getFloat32(4, true). encodeBatch: new ArrayBuffer(8 * points.length), offset = i * 8

    Загружаем среду выполнения...
    Загружаем AI-помощника...