`DataView` in TypeScript

`DataView` in TypeScript

Dieser Artikel erklärt DataView in TypeScript.

Wir erklären DataView in TypeScript Schritt für Schritt, von den Grundlagen bis zur praktischen Anwendung.

YouTube Video

DataView in TypeScript

DataView ermöglicht es Ihnen in TypeScript, feingranulare Lese- und Schreiboperationen auf Byte-Ebene eines ArrayBuffer durchzuführen. DataView ist äußerst nützlich für Low-Level-Binärverarbeitung wie Protokollimplementierung, Analyse von Binärdateien sowie das Senden und Empfangen von Binärdaten über WebSockets.

Grundlagen: Unterschiede zwischen ArrayBuffer, TypedArray und DataView

ArrayBuffer ist eine grundlegende Datenstruktur, die eine Folge von Bytes mit fester Länge speichert. TypedArrays wie Uint8Array sind Sichten, die den Buffer als Array eines bestimmten Typs behandeln, wobei jedes Element einen festen Typ hat.

Andererseits ist DataView eine flexible Ansicht, mit der Sie Werte jeglichen Datentyps an jeder beliebigen Offset-Position lesen und schreiben können. Da es keine festen Typen voraussetzt und eine feingranulare Kontrolle auf Byte-Ebene ermöglicht, eignet es sich für die Analyse binärer Protokolle und für die Verarbeitung auf niedriger Ebene.

Im Folgenden sehen Sie ein Beispiel, wie ein ArrayBuffer erstellt und anschließend sowohl ein Uint8Array als auch ein DataView daraus abgeleitet werden.

 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
  • In diesem Code werden zwei unterschiedliche Ansichten (Uint8Array und DataView) gleichzeitig auf demselben Buffer verwendet. Mit DataView können Sie flexibel Werte lesen und schreiben, indem Sie beliebige Offsets und die Byte-Reihenfolge (Endianness) angeben.

Hauptmethoden von DataView

DataView ist ein Interface zur Manipulation eines ArrayBuffer auf Byte-Ebene und bietet Methoden zum Lesen und Schreiben verschiedener Typen wie Integer und Gleitkommazahlen.

Die wichtigsten Methoden sind die folgenden:.

  • Lesen: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • Schreiben: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

Alle diese Methoden nehmen als ersten Parameter einen 'Byte-Offset': get liest den Wert an dieser Position und set schreibt den Wert dorthin. Bei 16-, 32- oder 64-Bit-Daten kann die Byte-Reihenfolge (Endianness) als zweites Argument angegeben werden. In der Praxis ist es am sichersten, die Endianness immer explizit gemäß der Datenspezifikation anzugeben.

Das folgende Beispiel zeigt, wie Sie eine 32-Bit-Ganzzahl und eine 32-Bit-Gleitkommazahl im Little-Endian-Format in einen Buffer schreiben und im gleichen Format wieder auslesen.

 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)
  • Durch die explizite Angabe der Endianness stellen Sie Kompatibilität mit unterschiedlichen Plattformen und Binärspezifikationen sicher.

Zum Thema Endianness (Byte-Reihenfolge)

Einige Protokolle, wie sie in Netzwerken verwendet werden, nutzen Big-Endian, während viele CPUs und Dateiformate vorwiegend Little-Endian verwenden. In DataView wird bei Übergabe von true als zweitem Argument die Daten als Little Endian interpretiert; bei false oder fehlendem Argument als Big Endian.

Im folgenden Beispiel sehen Sie, wie sich das Byte-Array im Speicher ändert, wenn dieselbe Zahl sowohl im Big-Endian- als auch im Little-Endian-Format geschrieben wird.

 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]
  • Das Verständnis, wie sich die Byte-Reihenfolge zwischen Big-Endian und Little-Endian ändert, erleichtert die Analyse von Kommunikationsdaten oder Binärformaten erheblich.

String-Ein-/Ausgabe (Verwendung von TextEncoder und TextDecoder)

DataView eignet sich hervorragend zum Lesen und Schreiben von Zahlen, behandelt aber keine Strings direkt. Üblicherweise werden Strings mit TextEncoder oder TextDecoder (z.B. als UTF-8) kodiert und dann mit Uint8Array in den Buffer kopiert. Das folgende Beispiel zeigt, wie Sie einen UTF-8-String in einen Buffer schreiben und wieder auslesen.

 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);
  • Indem Sie Strings in Binärdaten umwandeln und ggf. deren Länge voranstellen, können Sie Strings mit variabler Länge speichern.

Praktisches Beispiel: Kodieren/Dekodieren eines eigenen Binärformats

Im Folgenden definieren wir ein einfaches Nachrichtenformat, das eine Versionsnummer, eine ID und einen Namen enthält. Wir implementieren einen Kodierprozess, um Nachrichten in Binärdaten umzuwandeln, und einen Dekodierprozess, um das ursprüngliche Objekt aus den Binärdaten wiederherzustellen. In diesem Nachrichtenformat speichert das erste Byte die Version, die nächsten 4 Bytes speichern die ID im Little-Endian-Format, das darauf folgende Byte enthält die Länge des Namens und schließlich wird der utf-8-codierte Name gespeichert.

 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" }
  • Dies ist eine typische Umsetzung für ein Basis-Nachrichtenformat mit einem String variabler Länge und kann in vielen praktischen Szenarien wie Netzwerkommunikation oder eigenen Binärdateiformaten eingesetzt werden.

Hinweise und Best Practices

Beim Arbeiten mit Binärdaten über DataView müssen Sie neben dem einfachen Lesen und Schreiben Werte weitere Aspekte wie Sicherheit, konsequente Endianness und korrekte Typbehandlung beachten. Insbesondere beim Verarbeiten von Binärdaten aus externen Quellen oder beim Umgang mit großen Ganzzahlen ist es wichtig, den Code so zu gestalten, dass falsche Lesungen und Buffer-Überläufe verhindert werden. Nachfolgend finden Sie einige nützliche Hinweise für den praktischen Einsatz.

  • Grenzprüfung DataView löst eine Ausnahme aus, wenn der Offset oder die Größe die Buffer-Grenze überschreitet. Überprüfen Sie immer die Länge bei nicht vertrauenswürdigen Binärdaten.

  • Endianness immer explizit angeben Geben Sie in Ihrem Code immer explizit und konsequent Little-Endian oder Big-Endian an.

  • Typkonsistenz JavaScript-Zahlen sind 64-Bit IEEE-754-Gleitkommazahlen. Methoden wie getUint32 werden korrekt behandelt, aber es gibt kein getUint64, daher ist für 64-Bit-Ganzzahlen eine spezielle Behandlung nötig.

  • Umgang mit 64-Bit-Ganzzahlen Um 64-Bit-Ganzzahlen zu behandeln, müssen Sie BigInt verwenden oder den Wert manuell in High- und Low-32-Bit-Teile aufteilen. Hier ein einfaches Beispiel zum Auslesen eines 64-Bit-Unsigned-Integer.

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}
  • Mit BigInt können Sie auch Integer größer als 64 Bit sicher verarbeiten.

Einsatz unter Node.js (Zusammenspiel mit Buffer)

Obwohl Buffer in Node.js häufig verwendet wird, ist auch die Umwandlung zwischen ArrayBuffer und DataView einfach möglich. Verwenden Sie die buffer-Eigenschaft von Buffer-Objekten oder den Uint8Array-Konstruktor für die Umwandlung.

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));
  • Dies eignet sich, um Binärdaten zwischen Node.js und Browsern auszutauschen.

Zusammenfassung

DataView ist ein leistungsfähiger Mechanismus für flexibles Lesen und Schreiben von Binärdaten und besonders nützlich, wenn eine flexible Steuerung wie die Angabe der Endianness oder das Ansteuern beliebiger Positionen erforderlich ist. Durch die Kombination von ArrayBuffer, TypedArray und DataView können Sie Binärdaten flexibel und präzise in TypeScript verarbeiten und damit von Protokollimplementierung bis Datei-Analyse viele Anwendungsfälle abdecken. Mit der zusätzlichen Berücksichtigung von String-Kodierung und der Handhabung von 64-Bit-Ganzzahlen sind auch sehr praxisnahe Binäroperationen möglich.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video