`DataView` trong TypeScript

`DataView` trong TypeScript

Bài viết này giải thích về DataView trong TypeScript.

Chúng tôi sẽ giải thích về DataView trong TypeScript, từ cơ bản đến cách sử dụng thực tế, từng bước một.

YouTube Video

DataView trong TypeScript

Bằng cách sử dụng DataView trong TypeScript, bạn có thể thực hiện các thao tác đọc và ghi ở cấp độ byte một cách chi tiết trên một ArrayBuffer. DataView cực kỳ hữu ích cho các xử lý nhị phân cấp thấp như triển khai giao thức, phân tích tệp nhị phân và gửi/nhận dữ liệu nhị phân qua WebSocket.

Khái niệm cơ bản: Sự khác biệt giữa ArrayBuffer, TypedArray và DataView

ArrayBuffer là một cấu trúc dữ liệu cơ bản dùng để lưu trữ một dãy byte có độ dài cố định. TypedArray như Uint8Array là các view cho phép xử lý buffer dưới dạng một mảng của một kiểu dữ liệu cụ thể, trong đó mỗi phần tử đều có kiểu dữ liệu cố định.

Mặt khác, DataView là một chế độ xem linh hoạt cho phép bạn đọc và ghi giá trị của bất kỳ kiểu dữ liệu nào tại bất kỳ vị trí offset nào. Vì nó không giả định các kiểu dữ liệu cố định và cho phép kiểm soát chi tiết ở cấp độ byte, nên rất phù hợp để phân tích các giao thức nhị phân và xử lý ở cấp độ thấp.

Dưới đây là ví dụ về cách tạo một ArrayBuffer rồi tạo một Uint8ArrayDataView từ đó.

 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
  • Trong đoạn mã này, hai loại view khác nhau—Uint8ArrayDataView—được sử dụng đồng thời trên cùng một buffer. Bằng cách sử dụng DataView, bạn có thể linh hoạt đọc và ghi giá trị bằng cách chỉ định offset và thứ tự byte (endianness) tùy ý.

Các phương thức chính của DataView

DataView là một giao diện để thao tác với ArrayBuffer ở cấp byte, cung cấp các phương thức đọc và ghi nhiều kiểu dữ liệu như số nguyên và số thực dấu phẩy động.

Các phương thức chính như sau:.

  • Đọc: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • Ghi: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

Tất cả các phương thức này đều nhận 'byte offset' làm tham số đầu tiên: get để đọc giá trị tại vị trí đó, còn set để ghi giá trị vào vị trí đó. Bạn cũng có thể chỉ định thứ tự byte (endianness) bằng tham số thứ hai khi xử lý dữ liệu 16, 32 hoặc 64 bit. Trong thực tế, tốt nhất là luôn chỉ định rõ thứ tự byte (endianness) theo đặc tả dữ liệu.

Ví dụ dưới đây minh họa cách ghi một số nguyên 32 bit và một số thực 32 bit ở định dạng little endian vào buffer và đọc lại chúng ở cùng định dạng.

 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)
  • Bằng cách chỉ rõ thứ tự byte (endianness), bạn có thể đảm bảo tính tương thích với các nền tảng và đặc tả nhị phân khác nhau.

Về thứ tự byte (Endianness)

Một số giao thức như trong mạng sử dụng big-endian, trong khi nhiều CPU và định dạng tệp lại chủ yếu sử dụng thiết kế little-endian. Trong DataView, nếu tham số thứ hai được đặt là true, dữ liệu sẽ được xem là little endian; nếu đặt là false hoặc bỏ qua, nó sẽ coi là big endian.

Trong ví dụ sau, bạn có thể thấy cách mảng byte trong bộ nhớ thay đổi khi ghi cùng một số bằng cả hai định dạng big-endian và 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]
  • Hiểu được sự thay đổi thứ tự byte giữa big endian và little endian sẽ giúp việc phân tích dữ liệu truyền thông hoặc định dạng nhị phân dễ dàng hơn rất nhiều.

Nhập/Xuất chuỗi (Sử dụng kết hợp TextEncoderDecoder)

DataView rất tốt để đọc và ghi số, nhưng không xử lý chuỗi trực tiếp. Thông thường, chuỗi sẽ được mã hóa bằng TextEncoder hoặc TextDecoder (ví dụ sang UTF-8), sau đó copy vào buffer qua Uint8Array. Dưới đây là ví dụ về việc ghi một chuỗi UTF-8 vào buffer và đọc lại nó.

 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);
  • Bằng cách chuyển chuỗi sang nhị phân và thêm chiều dài chuỗi (nếu cần), bạn có thể lưu trữ các chuỗi có độ dài thay đổi.

Ví dụ thực tế: Mã hóa/Giải mã một định dạng nhị phân tùy chọn

Bên dưới, chúng tôi định nghĩa một định dạng thông điệp đơn giản bao gồm số phiên bản, ID và tên. Chúng tôi thực hiện một quá trình mã hóa để chuyển đổi thông điệp thành dữ liệu nhị phân, và một quá trình giải mã để khôi phục đối tượng ban đầu từ dữ liệu nhị phân. Trong định dạng thông điệp này, byte đầu tiên lưu trữ phiên bản, 4 byte tiếp theo lưu trữ ID theo định dạng little-endian, byte tiếp theo chứa độ dài tên, và cuối cùng là lưu trữ tên được mã hóa 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" }
  • Đây là một cách triển khai điển hình để xử lý định dạng message cơ bản có chứa chuỗi có độ dài thay đổi và có thể áp dụng trong nhiều tình huống thực tế như truyền thông qua mạng hoặc thiết kế tệp nhị phân tùy chỉnh.

Lưu ý và thực tiễn tốt nhất

Khi làm việc với dữ liệu nhị phân bằng DataView, có một số điểm thực tế cần lưu ý ngoài việc chỉ đọc và ghi giá trị — như an toàn, sử dụng thứ tự byte (endianness) một cách nhất quán và xử lý kiểu dữ liệu đúng đắn. Đặc biệt, khi xử lý dữ liệu nhị phân nhận từ nguồn bên ngoài hoặc làm việc với số nguyên lớn, điều quan trọng là thiết kế mã nguồn để tránh đọc sai hoặc tràn bộ đệm. Dưới đây là một số điểm hữu ích cần ghi nhớ khi sử dụng thực tế.

  • Kiểm tra giới hạn (Boundary Check) DataView sẽ phát sinh lỗi (exception) nếu offset hoặc kích thước vượt quá giới hạn của buffer. Luôn kiểm tra độ dài khi xử lý dữ liệu nhị phân không đáng tin cậy.

  • Luôn chỉ định rõ endianness Luôn luôn chỉ định rõ ràng và nhất quán little endian hoặc big endian trong mã nguồn của bạn.

  • Nhất quán kiểu dữ liệu Số trong JavaScript là các giá trị dấu phẩy động IEEE-754 64 bit. Các phương thức như getUint32 được xử lý phù hợp, nhưng không có getUint64, do đó cần xử lý đặc biệt với số nguyên 64 bit.

  • Xử lý số nguyên 64 bit Để xử lý số nguyên 64 bit, bạn cần dùng BigInt hoặc thủ công chia giá trị này thành hai phần 32 bit cao và thấp. Dưới đây là ví dụ đơn giản về cách đọc một số nguyên không dấu 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}
  • Nhờ sử dụng BigInt, bạn cũng có thể xử lý an toàn các số nguyên lớn hơn 64 bit.

Sử dụng trong Node.js (Tương tác với Buffer)

Mặc dù Buffer được sử dụng phổ biến trong Node.js, nhưng cũng rất dễ dàng chuyển đổi giữa ArrayBufferDataView. Hãy sử dụng thuộc tính buffer của đối tượng Buffer hoặc hàm tạo Uint8Array để chuyển đổi.

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));
  • Điều này có thể dùng như một công cụ để trao đổi dữ liệu nhị phân giữa Node.js và trình duyệt.

Tóm tắt

DataView là một cơ chế mạnh mẽ để đọc và ghi dữ liệu nhị phân một cách linh hoạt—đặc biệt hữu ích khi bạn cần kiểm soát như chỉ định endianness hoặc truy cập các vị trí tùy ý. Bằng cách kết hợp ArrayBuffer, TypedArrayDataView, bạn có thể xử lý dữ liệu nhị phân linh hoạt và chính xác với TypeScript, cho phép ứng dụng rộng rãi từ xây dựng giao thức đến phân tích tệp tin. Bằng việc bổ sung mã hóa chuỗi và xử lý số nguyên 64 bit khi cần thiết, bạn có thể thực hiện các thao tác xử lý nhị phân thực tế hơn nữa.

Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.

YouTube Video