`DataView` в TypeScript

`DataView` в TypeScript

В этой статье объясняется использование DataView в TypeScript.

Мы пошагово объясним DataView в TypeScript: от основ до практического применения.

YouTube Video

DataView в TypeScript

Используя DataView в TypeScript, вы можете выполнять точечные операции чтения и записи на уровне байтов в ArrayBuffer. DataView крайне полезен для низкоуровневой обработки бинарных данных, такой как реализация протоколов, анализ бинарных файлов и отправка/получение бинарных данных через WebSocket.

Основные понятия: различия между ArrayBuffer, TypedArray и DataView

ArrayBuffer — это базовая структура данных, используемая для хранения последовательности байтов фиксированной длины. TypedArray (например, Uint8Array) — это представления, которые воспринимают буфер как массив определённого типа, где каждый элемент имеет фиксированный тип.

С другой стороны, DataView — это гибкое представление, которое позволяет читать и записывать значения любого типа данных по любому смещению. Поскольку он не предполагает фиксированные типы и позволяет осуществлять тонкий контроль на уровне байтов, он подходит для анализа бинарных протоколов и низкоуровневой обработки.

Ниже приведён пример создания ArrayBuffer, а затем создания из него Uint8Array и DataView.

 1// Create an ArrayBuffer (8 bytes)
 2const buffer = new ArrayBuffer(8);
 3
 4// Create a Uint8Array view (easy to manipulate individual bytes)
 5const u8 = new Uint8Array(buffer);
 6u8[0] = 0x12;
 7u8[1] = 0x34;
 8
 9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
  • В этом коде два разных типа представлений — Uint8Array и DataView — используются одновременно с одним буфером. Используя DataView, вы можете гибко читать и записывать значения, указывая произвольные смещения и порядок байт (эндийность).

Основные методы DataView

DataView — это интерфейс для работы с ArrayBuffer на уровне байтов, предоставляющий методы для чтения и записи различных типов, таких как целые числа и числа с плавающей запятой.

Основные методы следующие:.

  • Чтение: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • Запись: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

Все эти методы принимают 'смещение в байтах' в качестве первого аргумента: get читает значение по этому смещению, а set — записывает. Вы также можете указать порядок байт (эндийность) вторым аргументом при работе с 16-, 32- или 64-битными данными. На практике наиболее безопасно всегда явно указывать эндийность в соответствии со спецификацией данных.

Следующий пример показывает, как записать 32-битное целое число и 32-битное число с плавающей запятой в формате little endian в буфер, а затем прочитать их в том же формате.

 1const buf = new ArrayBuffer(12); // Enough space for integer + float
 2const view = new DataView(buf);
 3
 4// Write a 32-bit unsigned integer at offset 0 (little-endian)
 5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
 6
 7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
 8const intVal = view.getUint32(0, true);
 9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
  • Явно указывая эндийность, вы обеспечиваете совместимость с разными платформами и спецификациями бинарных форматов.

О порядке байт (эндийность)

Некоторые протоколы, такие как сетевые, используют big-endian, тогда как многие процессоры и форматы файлов в основном используют little-endian. В DataView, если второй аргумент равен true, данные интерпретируются как little endian; если false или аргумент опущен — как big endian.

В следующем примере показано, как изменяется массив байтов в памяти при записи одного и того же числа в форматах big-endian и little-endian.

 1const b = new ArrayBuffer(4);
 2const a = new Uint8Array(b);
 3const dv = new DataView(b);
 4
 5// Write in big-endian order
 6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
 7console.log([...a]); // [10, 11, 12, 13]
 8
 9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
  • Понимание различий порядка байт между big endian и little endian значительно упростит анализ передаваемых данных и бинарных форматов.

Ввод/вывод строк (вместе с использованием TextEncoder и TextDecoder)

DataView отлично подходит для чтения и записи чисел, но не работает напрямую со строками. Обычно строки кодируются с помощью TextEncoder или TextDecoder (например, в UTF-8), а затем копируются в буфер через Uint8Array. Ниже представлен пример записи строки в формате UTF-8 в буфер и последующего её чтения.

 1const encoder = new TextEncoder();
 2const decoder = new TextDecoder();
 3
 4const str = "\u3053\u3093\u306b\u3061\u306f";
 5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
 6
 7// Create a buffer and write the encoded string bytes into it
 8const buf2 = new ArrayBuffer(encoded.length);
 9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);
  • Преобразовав строки в бинарный формат и, при необходимости, добавив их длину в начало, вы можете хранить строки переменной длины.

Практический пример: кодирование и декодирование пользовательского бинарного формата

Ниже мы определяем простой формат сообщения, содержащий номер версии, идентификатор (ID) и имя. Мы реализуем процесс кодирования для преобразования сообщений в бинарные данные и процесс декодирования для восстановления исходного объекта из бинарных данных. В этом формате сообщения первый байт содержит версию, следующие 4 байта содержат идентификатор в формате little-endian, далее идет байт с длиной имени, и, наконец, хранится имя в кодировке UTF-8.

 1// Encode an object into a binary message
 2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
 3  const encoder = new TextEncoder();
 4  const nameBytes = encoder.encode(msg.name);
 5  const total = 1 + 4 + 1 + nameBytes.length;
 6  const buf = new ArrayBuffer(total);
 7  const dv = new DataView(buf);
 8  let offset = 0;
 9
10  dv.setUint8(offset, msg.version); offset += 1;      // write version
11  dv.setUint32(offset, msg.id, true); offset += 4;    // write ID (little-endian)
12  dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14  // write name bytes
15  new Uint8Array(buf, offset).set(nameBytes);
16  return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21  const dv = new DataView(buf);
22  let offset = 0;
23
24  const version = dv.getUint8(offset); offset += 1;   // read version
25  const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26  const nameLen = dv.getUint8(offset); offset += 1;   // read name length
27
28  // extract name bytes
29  const nameBytes = new Uint8Array(buf, offset, nameLen);
30  const decoder = new TextDecoder();
31  // decode UTF-8 string
32  const name = decoder.decode(nameBytes);
33
34  return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
  • Это типичная реализация работы с простым форматом сообщения, содержащим строку переменной длины, и может применяться во многих практических сценариях, таких как сетевые коммуникации или пользовательские бинарные форматы файлов.

Предостережения и лучшие практики

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

  • Проверка границ DataView выбрасывает исключение, если смещение или размер выходят за пределы буфера. Всегда проверяйте длины при работе с недоверенными бинарными данными.

  • Всегда указывайте эндийность Всегда явно и последовательно указывайте little endian или big endian в вашем коде.

  • Согласованность типов Числа в JavaScript — это 64-битные числа с плавающей запятой по стандарту IEEE-754. Методы, такие как getUint32, работают корректно, но метода getUint64 не существует, поэтому для 64-битных целых требуется особая обработка.

  • Работа с 64-битными целыми числами Для работы с 64-битными целыми числами используйте BigInt либо вручную разделяйте значение на 32-битные старшие и младшие части. Вот простой пример чтения 64-битного беззнакового целого числа.

1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2  const low = BigInt(dv.getUint32(offset, littleEndian));
3  const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4  return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}
  • Используя BigInt, вы также сможете безопасно работать с целыми числами больше 64 бит.

Использование в Node.js (взаимодействие с Buffer)

Хотя Buffer часто используется в Node.js, также легко производить преобразования между ArrayBuffer и DataView. Для преобразования используйте свойство buffer у объектов Buffer или конструктор Uint8Array.

1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));
  • Это может использоваться как средство обмена бинарными данными между Node.js и браузерами.

Резюме

DataView — это мощный инструмент для свободного чтения и записи бинарных данных, особенно полезный там, где требуется гибкое управление — например, указание эндийности или доступ к произвольным байтам. Комбинируя ArrayBuffer, TypedArray и DataView, вы сможете гибко и точно работать с бинарными данными в TypeScript — от реализации протоколов до анализа файлов. Дополнительно используя кодирование строк и работу с 64-битными целыми по мере необходимости, вы сможете выполнять ещё более практичные бинарные операции.

Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.

YouTube Video