TypeScript 中的 `DataView`

TypeScript 中的 `DataView`

本文解释了 TypeScript 中的 DataView

我们将一步步讲解 TypeScript 中的 DataView,从基础到实际应用。

YouTube Video

TypeScript 中的 DataView

通过在 TypeScript 中使用 DataView,你可以在 ArrayBuffer 上执行细粒度的字节级读写操作。DataView 在协议实现、二进制文件解析以及通过 WebSocket 发送/接收二进制数据等底层二进制处理场景中非常有用。

基础概念:ArrayBuffer、TypedArray 和 DataView 的区别

ArrayBuffer 是一种用于存储固定长度字节序列的基础数据结构。像 Uint8Array 这样的 TypedArray 是将缓冲区视为特定类型数组的视图,每个元素具有固定的数据类型。

另一方面,DataView 是一个灵活的视图,允许你在任何偏移位置读取和写入任意数据类型的值。由于它不假设固定的数据类型,并允许在字节级别进行精细控制,非常适合分析二进制协议和底层处理。

下面是一个创建 ArrayBuffer,然后从中创建 Uint8ArrayDataView 的示例。

 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
  • 在这段代码中,Uint8ArrayDataView 这两种不同类型的视图同时操作同一个缓冲区。使用 DataView,可以灵活地通过指定任意偏移和字节序来读写数值。

DataView 的主要方法

DataView 是一种在字节级别操作 ArrayBuffer 的接口,提供了读取和写入各种类型(如整数、浮点数)的方法。

主要方法如下:。

  • 读取getUint8getInt8getUint16getInt16getUint32getInt32getFloat32getFloat64
  • 写入setUint8setInt8setUint16setInt16setUint32setInt32setFloat32setFloat64

所有这些方法都将“字节偏移量”作为第一个参数:get 方法读取该位置的值,set 方法将值写入该位置。在处理 16、32 或 64 位数据时,还可以通过第二个参数指定字节序(大端或小端)。实际使用中,按照数据规范始终明确指定字节序最为安全。

下面的示例演示了如何以小端格式向缓冲区写入 32 位整数和 32 位浮点数,然后以相同格式读出。

 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)
  • 通过显式指定字节序,可以确保与不同平台和二进制规范的兼容性。

关于字节序(字节顺序)

有些协议(如网络通信协议)采用大端序,而许多 CPU 和文件格式则以小端序为主。在 DataView 中,第二个参数为 true 时视为小端序,为 false 或省略时视为大端序

在下面的例子中,你可以看到用大端和小端格式写入相同数字时,内存中的字节数组是如何变化的。

 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]
  • 理解大端序和小端序下的字节顺序变化,将有助于更轻松地分析通信数据和二进制格式。

字符串输入/输出(结合使用 TextEncoderDecoder

DataView 非常适合数值的读写,但无法直接处理字符串。通常做法是使用 TextEncoderTextDecoder(比如编码为 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 个字节以小端格式存储 ID,之后的一个字节存储名称长度,最后存储用 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 会抛出异常。处理不可信二进制数据时务必检查长度。

  • 始终明确指定字节序 在代码中始终明确且一致地指定小端或大端序。

  • 类型一致性 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 中常用,但在 ArrayBufferDataView 之间转换也很容易。可以使用 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 是用于灵活读取和写入二进制数据的强大机制,特别适用于需要指定字节序和访问任意位置等灵活操作的场景。通过组合使用 ArrayBufferTypedArrayDataView,可以在 TypeScript 中灵活且准确地处理二进制数据,适用于协议实现、文件分析等多种场景。结合字符串编码和 64 位整数的处理,可以实现更实用的二进制操作。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video