`DataView` en TypeScript

`DataView` en TypeScript

Este artículo explica DataView en TypeScript.

Explicaremos DataView en TypeScript, desde los conceptos básicos hasta el uso práctico, paso a paso.

YouTube Video

DataView en TypeScript

Al usar DataView en TypeScript, puedes realizar operaciones de lectura y escritura a nivel de byte en un ArrayBuffer con gran precisión. DataView es sumamente útil para el procesamiento binario de bajo nivel, como la implementación de protocolos, el análisis de archivos binarios y el envío/recepción de datos binarios a través de WebSocket.

Conceptos básicos: Diferencias entre ArrayBuffer, TypedArray y DataView

ArrayBuffer es una estructura de datos fundamental utilizada para almacenar una secuencia de bytes de longitud fija. TypedArrays como Uint8Array son vistas que tratan el buffer como un arreglo de un tipo específico, donde cada elemento tiene un tipo fijo.

Por otro lado, DataView es una vista flexible que permite leer y escribir valores de cualquier tipo de dato en cualquier posición de desplazamiento. Como no asume tipos fijos y permite un control detallado a nivel de byte, es adecuado para analizar protocolos binarios y para el procesamiento de bajo nivel.

A continuación, se muestra un ejemplo de cómo crear un ArrayBuffer y luego crear un Uint8Array y un DataView a partir de él.

 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
  • En este código, se utilizan simultáneamente dos tipos diferentes de vistas—Uint8Array y DataView—sobre un solo buffer. Con DataView, puedes leer y escribir valores de manera flexible, especificando posiciones y el orden de los bytes (endianness) de forma arbitraria.

Métodos principales de DataView

DataView es una interfaz para manipular un ArrayBuffer a nivel de bytes, y proporciona métodos para leer y escribir diversos tipos como enteros y números de punto flotante.

Los métodos principales son los siguientes:.

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

Todos estos métodos toman una 'posición en bytes' como primer argumento: get lee el valor en esa posición y set escribe el valor en esa posición. También puedes especificar el endianness como segundo argumento al manejar datos de 16, 32 o 64 bits. En la práctica, lo más seguro es especificar siempre el endianness de acuerdo con la especificación de los datos.

El siguiente ejemplo muestra cómo escribir un entero de 32 bits y un número de punto flotante de 32 bits en formato little endian en un buffer, y luego leerlos de nuevo con el mismo 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)
  • Al especificar explícitamente el endianness, puedes asegurar la compatibilidad con distintas plataformas y especificaciones binarias.

Sobre el Endianness (orden de los bytes)

Algunos protocolos, como los usados en redes, emplean big-endian, mientras que muchas CPUs y formatos de archivo utilizan principalmente diseño little-endian. En DataView, si el segundo argumento se establece en true, trata los datos como little endian; si se establece como false o se omite, los trata como big endian.

En el siguiente ejemplo, se puede ver cómo cambia el arreglo de bytes en memoria al escribir el mismo número utilizando los formatos big-endian y 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]
  • Comprender cómo cambia el orden de los bytes entre big endian y little endian facilitará mucho el análisis de datos de comunicación o formatos binarios.

Entrada/Salida de cadenas (usando TextEncoder y Decoder juntos)

DataView es excelente para leer y escribir números, pero no maneja cadenas de texto directamente. Es común codificar las cadenas usando TextEncoder o TextDecoder (por ejemplo, a UTF-8), y luego copiarlas al buffer mediante Uint8Array. A continuación se muestra un ejemplo de cómo escribir una cadena UTF-8 en un buffer y luego leerla de vuelta.

 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);
  • Al convertir cadenas a binario y, si es necesario, anteponer su longitud, puedes almacenar cadenas de longitud variable.

Ejemplo práctico: Codificación/Decodificación de un formato binario personalizado

A continuación, definimos un formato de mensaje sencillo que contiene un número de versión, ID y nombre. Implementamos un proceso de codificación para convertir mensajes a datos binarios y un proceso de decodificación para restaurar el objeto original desde el binario. En este formato de mensaje, el primer byte almacena la versión, los siguientes 4 bytes almacenan el ID en formato little-endian, el byte siguiente contiene la longitud del nombre y, finalmente, se almacena el nombre codificado en 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" }
  • Esta es una implementación típica para manejar un formato de mensaje básico que contiene una cadena de longitud variable y puede aplicarse en muchas situaciones prácticas, como la comunicación en red o diseños de archivos binarios personalizados.

Precauciones y mejores prácticas

Al trabajar con datos binarios usando DataView, hay varios puntos prácticos que considerar más allá de simplemente leer y escribir valores, como la seguridad, el uso consistente del endianness y el manejo adecuado de los tipos de datos. En particular, al manejar datos binarios recibidos de fuentes externas o tratar con números enteros grandes, es importante diseñar tu código para prevenir lecturas incorrectas y desbordamientos del buffer. A continuación, se presentan algunos puntos útiles a tener en cuenta para su uso práctico.

  • Comprobación de límites DataView lanza una excepción si un desplazamiento o tamaño excede los límites del buffer. Verifica siempre las longitudes al tratar con datos binarios no confiables.

  • Especifica siempre el endianness Especifica de manera explícita y consistente little endian o big endian en tu código.

  • Consistencia de tipos Los números en JavaScript son valores de punto flotante IEEE-754 de 64 bits. Métodos como getUint32 se manejan correctamente, pero no existe getUint64, por lo que se requiere un manejo especial para enteros de 64 bits.

  • Manejo de enteros de 64 bits Para manejar enteros de 64 bits, necesitas usar BigInt o dividir manualmente el valor en partes alta y baja de 32 bits. Aquí tienes un ejemplo sencillo de cómo leer un entero sin signo de 64 bits.

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}
  • Al usar BigInt, puedes manejar de forma segura enteros mayores a 64 bits también.

Uso en Node.js (interoperando con Buffer)

Aunque Buffer se utiliza comúnmente en Node.js, también es fácil convertir entre ArrayBuffer y DataView. Utiliza la propiedad buffer de los objetos Buffer o el constructor de Uint8Array para la conversión.

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));
  • Esto se puede usar como una herramienta para intercambiar datos binarios entre Node.js y los navegadores.

Resumen

DataView es un mecanismo potente para leer y escribir datos binarios libremente—particularmente útil en situaciones donde se necesita un control flexible, como especificar el endianness y acceder a posiciones arbitrarias. Combinando ArrayBuffer, TypedArray y DataView, puedes manejar datos binarios de forma flexible y precisa con TypeScript, habilitando una amplia gama de casos de uso, desde la implementación de protocolos hasta el análisis de archivos. Al incorporar también la codificación de cadenas y el manejo de enteros de 64 bits según sea necesario, puedes realizar operaciones binarias aún más prácticas.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video