`DataView` in TypeScript
Questo articolo spiega il DataView in TypeScript.
Spiegheremo DataView in TypeScript, dalle basi all’uso pratico, passo dopo passo.
YouTube Video
DataView in TypeScript
Utilizzando DataView in TypeScript, puoi eseguire operazioni di lettura e scrittura a livello di byte su un ArrayBuffer con grande precisione. DataView è estremamente utile per la gestione binaria a basso livello, come l’implementazione di protocolli, l’analisi di file binari e l’invio/ricezione di dati binari tramite WebSocket.
Concetti di base: differenze tra ArrayBuffer, TypedArray e DataView
ArrayBuffer è una struttura dati fondamentale utilizzata per memorizzare una sequenza di byte di lunghezza fissa. I TypedArray come Uint8Array sono viste che trattano il buffer come un array di un tipo specifico, in cui ogni elemento ha un tipo prefissato.
D'altra parte, DataView è una vista flessibile che consente di leggere e scrivere valori di qualsiasi tipo di dato a qualsiasi posizione di offset. Poiché non presuppone tipi fissi e consente un controllo dettagliato a livello di byte, è adatto per l'analisi di protocolli binari e l'elaborazione di basso livello.
Di seguito è riportato un esempio di creazione di un ArrayBuffer, seguito dalla creazione di un Uint8Array e di un DataView a partire da esso.
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 questo codice, due diversi tipi di viste—
Uint8ArrayeDataView—sono utilizzate contemporaneamente sullo stesso buffer. UsandoDataView, puoi leggere e scrivere valori in modo flessibile specificando offset e endianness arbitrari.
Metodi principali di DataView
DataView è un’interfaccia per manipolare un ArrayBuffer a livello di byte che mette a disposizione metodi per leggere e scrivere vari tipi di dati come interi e numeri in virgola mobile.
I metodi principali sono i seguenti:.
- Lettura:
getUint8,getInt8,getUint16,getInt16,getUint32,getInt32,getFloat32,getFloat64 - Scrittura:
setUint8,setInt8,setUint16,setInt16,setUint32,setInt32,setFloat32,setFloat64
Tutti questi metodi prendono come primo argomento un 'byte offset': con get si legge il valore in quella posizione e con set si scrive il valore in quella posizione. Puoi anche specificare l'endianness tramite il secondo argomento quando gestisci dati da 16, 32 o 64 bit. Nella pratica, è più sicuro specificare sempre l'endianess secondo la specifica dei dati.
Il seguente esempio mostra come scrivere un intero a 32 bit e un numero in virgola mobile a 32 bit in formato little endian in un buffer e poi leggerli nello stesso formato.
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)
- Specificando esplicitamente l'endianess, puoi garantire la compatibilità tra diverse piattaforme e specifiche binarie.
Informazioni sull’endianness (ordine dei byte)
Alcuni protocolli, come quelli di rete, utilizzano il formato big-endian, mentre molte CPU e formati di file usano principalmente little-endian. In DataView, se il secondo argomento è impostato su true, i dati vengono trattati come little endian; se impostato su false o omesso, vengono trattati come big endian.
Nel seguente esempio, si può vedere come l'array di byte in memoria cambia quando si scrive lo stesso numero utilizzando sia il formato big-endian che quello 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]
- Comprendere come cambia l’ordine dei byte tra big endian e little endian rende molto più semplice analizzare dati di comunicazione o formati binari.
Input/Output di stringhe (utilizzando insieme TextEncoder e Decoder)
DataView è ottimo per la lettura e scrittura di numeri, ma non gestisce direttamente le stringhe. Di solito si codificano le stringhe usando TextEncoder o TextDecoder (ad esempio in UTF-8), quindi si copiano nel buffer tramite Uint8Array. Ecco un esempio di scrittura di una stringa UTF-8 in un buffer e successiva lettura.
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);- Convertendo le stringhe in binario e, se necessario, inserendo la lunghezza all’inizio, puoi memorizzare stringhe di lunghezza variabile.
Esempio pratico: codifica/decodifica di un formato binario personalizzato
Di seguito definiamo un semplice formato di messaggio contenente un numero di versione, un ID e un nome. Implementiamo un processo di codifica per convertire i messaggi in dati binari e un processo di decodifica per ripristinare l'oggetto originale dai dati binari. In questo formato di messaggio, il primo byte memorizza la versione, i successivi 4 byte memorizzano l'ID in formato little-endian, il byte successivo contiene la lunghezza del nome e infine viene memorizzato il nome codificato in 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" }
- Questa è un’implementazione tipica per gestire un formato di messaggio di base che contiene una stringa a lunghezza variabile e può essere applicata in molte situazioni pratiche come la comunicazione di rete o la progettazione di file binari personalizzati.
Avvertenze e buone pratiche
Quando si lavora con dati binari utilizzando DataView, ci sono diversi aspetti pratici da considerare, oltre alla semplice lettura e scrittura dei valori, come la sicurezza, l’uso coerente dell’endianess e la corretta gestione dei tipi. In particolare, quando si trattano dati binari provenienti da fonti esterne o valori interi molto grandi, è importante progettare il codice per prevenire letture errate e accessi fuori dai limiti del buffer. Di seguito sono riportati alcuni punti utili da ricordare per l'utilizzo pratico.
-
Controllo dei limiti
DataViewgenera un’eccezione se un offset o una dimensione supera i limiti del buffer. Verifica sempre le lunghezze quando gestisci dati binari non attendibili. -
Specifica sempre l’endianess Specifica sempre in modo esplicito e coerente little endian o big endian nel tuo codice.
-
Coerenza dei tipi I numeri in JavaScript sono valori in virgola mobile a 64 bit secondo lo standard IEEE-754. Metodi come
getUint32sono gestiti correttamente, ma non esistegetUint64, pertanto è necessario un trattamento speciale per gli interi a 64 bit. -
Gestione degli interi a 64 bit Per gestire gli interi a 64 bit, è necessario utilizzare
BigIntoppure dividere manualmente il valore in parti alte e basse da 32 bit. Ecco un semplice esempio di lettura di un intero senza segno a 64 bit.
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}- Utilizzando
BigInt, puoi gestire in sicurezza anche interi superiori a 64 bit.
Utilizzo in Node.js (interoperabilità con Buffer)
Anche se Buffer è comunemente usato in Node.js, è anche facile convertire tra ArrayBuffer e DataView. Usa la proprietà buffer degli oggetti Buffer oppure il costruttore Uint8Array per la conversione.
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));- Questo può essere utilizzato come strumento per lo scambio di dati binari tra Node.js e browser.
Riepilogo
DataView è un meccanismo potente per leggere e scrivere dati binari in modo flessibile—particolarmente utile quando hai bisogno di specificare l’endianess o accedere a posizioni arbitrarie. Combinando ArrayBuffer, TypedArray e DataView, puoi gestire dati binari in modo flessibile e preciso con TypeScript, coprendo un’ampia gamma di possibili utilizzi, dall’implementazione di protocolli all’analisi di file. Includendo anche la codifica delle stringhe e la gestione degli interi a 64 bit quando necessario, è possibile realizzare operazioni binarie ancora più pratiche.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.