`DataView` w TypeScript
Ten artykuł wyjaśnia DataView w TypeScript.
Wyjaśnimy DataView w TypeScript krok po kroku – od podstaw do praktycznych zastosowań.
YouTube Video
DataView w TypeScript
Korzystając z DataView w TypeScript, możesz wykonywać precyzyjne operacje odczytu i zapisu na poziomie bajtów w obiekcie ArrayBuffer. DataView jest niezwykle przydatny do niskopoziomowego przetwarzania binarnego, takiego jak implementacja protokołów, analiza plików binarnych i wysyłanie/odbieranie danych binarnych przez WebSocket.
Podstawowe pojęcia: różnice między ArrayBuffer, TypedArray i DataView
ArrayBuffer to podstawowa struktura danych służąca do przechowywania sekwencji bajtów o stałej długości. TypedArray, takie jak Uint8Array, to widoki traktujące bufor jako tablicę konkretnego typu, gdzie każdy element ma ustalony typ.
Z drugiej strony, DataView to elastyczny widok, który pozwala odczytywać i zapisywać wartości dowolnego typu danych na dowolnej pozycji przesunięcia. Ponieważ nie zakłada ustalonych typów i umożliwia precyzyjną kontrolę na poziomie bajtów, jest odpowiedni do analizy protokołów binarnych oraz niskopoziomowego przetwarzania.
Poniżej znajduje się przykład utworzenia ArrayBuffer, a następnie Uint8Array i DataView na jego podstawie.
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
- W tym kodzie dwa różne typy widoków —
Uint8ArrayiDataView— są używane jednocześnie na jednym buforze. Korzystając zDataView, możesz elastycznie odczytywać i zapisywać wartości, określając dowolne przesunięcia i kolejność bajtów (endianness).
Główne metody DataView
DataView to interfejs do manipulowania ArrayBuffer na poziomie bajtów, zapewniający metody do odczytu i zapisu różnych typów, takich jak liczby całkowite i zmiennoprzecinkowe.
Główne metody to:.
- Odczyt:
getUint8,getInt8,getUint16,getInt16,getUint32,getInt32,getFloat32,getFloat64 - Zapis:
setUint8,setInt8,setUint16,setInt16,setUint32,setInt32,setFloat32,setFloat64
Wszystkie te metody przyjmują jako pierwszy argument 'przesunięcie bajtowe': get odczytuje wartość z tej pozycji, a set zapisuje wartość na tej pozycji. Możesz także określić kolejność bajtów (endianness) drugim argumentem podczas operowania na danych 16-, 32- lub 64-bitowych. W praktyce najbezpieczniej jest zawsze jawnie określać endianness zgodnie ze specyfikacją danych.
Poniższy przykład pokazuje, jak zapisać liczbę całkowitą 32-bitową oraz liczbę zmiennoprzecinkową 32-bitową w formacie little endian do bufora i następnie je z niego odczytać w tym samym formacie.
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)
- Poprzez jawne określenie endianness możesz zapewnić kompatybilność z różnymi platformami i specyfikacjami binarnymi.
O endianness (kolejność bajtów)
Niektóre protokoły, na przykład sieciowe, używają big-endian, podczas gdy wiele procesorów i formatów plików głównie stosuje little-endian. W DataView, jeśli drugi argument zostanie ustawiony jako true, dane traktowane są jako little endian; jeśli ustawiony jako false lub pominięty — jako big endian.
W poniższym przykładzie możesz zobaczyć, jak zmienia się tablica bajtów w pamięci podczas zapisywania tej samej liczby zarówno w formacie big-endian, jak i 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]
- Zrozumienie zmian kolejności bajtów między big endian a little endian znacznie ułatwi analizę danych komunikacyjnych lub formatów binarnych.
Odczyt i zapis napisów (przy użyciu TextEncoder oraz TextDecoder)
DataView świetnie nadaje się do odczytu i zapisu liczb, ale nie obsługuje bezpośrednio napisów. Typowym rozwiązaniem jest kodowanie napisów przy pomocy TextEncoder lub TextDecoder (np. do UTF-8), a następnie kopiowanie ich do bufora przez Uint8Array. Poniżej znajduje się przykład zapisu napisu w UTF-8 do bufora oraz jego ponownego odczytu.
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);- Poprzez konwersję napisów do postaci binarnej oraz – w razie potrzeby – poprzedzając je ich długością, możesz przechowywać napisy o zmiennej długości.
Praktyczny przykład: kodowanie/dekodowanie własnego formatu binarnego
Poniżej definiujemy prosty format wiadomości zawierający numer wersji, identyfikator i nazwę. Implementujemy proces kodowania, aby przekształcić wiadomości na dane binarne oraz proces dekodowania w celu odtworzenia oryginalnego obiektu z danych binarnych. W tym formacie wiadomości pierwszy bajt przechowuje wersję, kolejne 4 bajty identyfikator w formacie little-endian, następny bajt zawiera długość nazwy, a na końcu zapisana jest nazwa zakodowana w 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" }
- To typowa implementacja obsługi prostego formatu wiadomości zawierającego napis o zmiennej długości, którą można wykorzystać w wielu praktycznych przypadkach, takich jak komunikacja sieciowa czy własne formaty plików binarnych.
Uwagi i najlepsze praktyki
Podczas pracy z danymi binarnymi z użyciem DataView, należy wziąć pod uwagę kilka praktycznych kwestii, takich jak bezpieczeństwo, konsekwentne stosowanie endianness oraz poprawna obsługa typów – wykraczające poza sam odczyt i zapis wartości. W szczególności przy obsłudze danych binarnych z zewnętrznych źródeł lub podczas pracy z dużymi liczbami całkowitymi należy zadbać o projekt kodu zapobiegający błędnemu odczytowi i przekroczeniu granic bufora. Poniżej znajdują się przydatne wskazówki, o których warto pamiętać podczas praktycznego użycia.
-
Sprawdzanie granic
DataViewzgłosi wyjątek, jeśli przesunięcie lub rozmiar przekroczy granicę bufora. Zawsze sprawdzaj długości podczas pracy z niezaufanymi danymi binarnymi. -
Zawsze określaj endianness Zawsze jawnie i konsekwentnie określaj, czy używasz little endian, czy big endian w swoim kodzie.
-
Spójność typów Liczby w JavaScript to wartości zmiennoprzecinkowe 64-bitowe zgodne ze standardem IEEE-754. Metody takie jak
getUint32są obsługiwane prawidłowo, ale nie istniejegetUint64, więc liczby całkowite 64-bitowe wymagają specjalnej obsługi. -
Obsługa liczb całkowitych 64-bitowych Aby obsłużyć liczby całkowite 64-bitowe, należy użyć
BigIntlub ręcznie podzielić wartość na wysoką i niską część 32-bitową. Oto prosty przykład odczytu 64-bitowej liczby całkowitej bez znaku.
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}- Korzystając z
BigInt, możesz bezpiecznie obsługiwać także liczby całkowite większe niż 64-bitowe.
Użycie w Node.js (współpraca z Buffer)
Chociaż Buffer jest powszechnie używany w Node.js, łatwo jest również konwertować między ArrayBuffer a DataView. Do konwersji użyj właściwości buffer z obiektów Buffer lub konstruktora 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));- Może to służyć jako narzędzie do wymiany danych binarnych pomiędzy Node.js a przeglądarkami.
Podsumowanie
DataView to potężny mechanizm pozwalający na swobodne odczytywanie i zapisywanie danych binarnych — szczególnie przydatny, gdy potrzebna jest elastyczna kontrola, np. podczas określania endianness lub pracy z dowolnymi pozycjami. Łącząc ArrayBuffer, TypedArray oraz DataView, możesz elastycznie i precyzyjnie obsługiwać dane binarne w TypeScript, uzyskując szeroki zakres zastosowań – od implementacji protokołów po analizę plików. Uzupełniając to o kodowanie napisów oraz obsługę liczb całkowitych 64-bitowych (w razie potrzeby), możesz realizować jeszcze bardziej zaawansowane operacje binarne.
Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.