`DataView` ใน TypeScript

`DataView` ใน TypeScript

บทความนี้อธิบายเกี่ยวกับ DataView ใน TypeScript

เราจะอธิบายการใช้งาน DataView ใน TypeScript ตั้งแต่พื้นฐานจนถึงการใช้งานจริงทีละขั้นตอน

YouTube Video

DataView ใน TypeScript

โดยการใช้ DataView ใน TypeScript คุณสามารถอ่านและเขียนข้อมูลในระดับไบต์อย่างละเอียดบน ArrayBuffer ได้ DataView มีประโยชน์มากสำหรับการประมวลผลไบนารีระดับต่ำ เช่น การสร้างโปรโตคอล, การวิเคราะห์ไฟล์ไบนารี และการส่ง/รับข้อมูลไบนารีผ่าน WebSocket

แนวคิดพื้นฐาน: ความแตกต่างระหว่าง ArrayBuffer, TypedArray และ DataView

ArrayBuffer เป็นโครงสร้างข้อมูลพื้นฐานที่ใช้เก็บชุดของข้อมูลไบต์ที่มีความยาวคงที่ TypedArray เช่น Uint8Array เป็นวิวที่มองบัฟเฟอร์เป็นอาเรย์ของชนิดข้อมูลแบบเฉพาะ โดยแต่ละองค์ประกอบจะมีชนิดข้อมูลคงที่

ในทางกลับกัน DataView เป็นวิวที่มีความยืดหยุ่นซึ่งช่วยให้คุณสามารถอ่านและเขียนค่าของประเภทข้อมูลใดๆ ได้ที่ตำแหน่งออฟเซตใดๆ เนื่องจากไม่ได้กำหนดประเภทข้อมูลตายตัวและสามารถควบคุมได้อย่างละเอียดในระดับไบต์ จึงเหมาะสำหรับการวิเคราะห์โปรโตคอลไบนารีและการประมวลผลระดับล่าง

ตัวอย่างต่อไปนี้เป็นการสร้าง ArrayBuffer และตามด้วยการสร้าง Uint8Array และ DataView จากมัน

 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
  • ในโค้ดนี้ จะมีการใช้วิวสองแบบ คือ Uint8Array และ DataView กับบัฟเฟอร์เดียวกันพร้อมกัน ด้วยการใช้ DataView คุณสามารถอ่านและเขียนค่าได้อย่างยืดหยุ่นโดยระบุตำแหน่งไบต์และ endianness ได้เอง

เมธอดหลักของ DataView

DataView เป็นอินเทอร์เฟซสำหรับจัดการ ArrayBuffer ในระดับไบต์ โดยมีเมธอดสำหรับอ่านและเขียนข้อมูลในหลายชนิด เช่น จำนวนเต็มและเลขทศนิยม

เมธอดหลักมีดังนี้:

  • อ่าน: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • เขียน: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

เมธอดเหล่านี้ทั้งหมดจะต้องระบุตำแหน่ง 'byte offset' เป็นอาร์กิวเมนต์แรก: get จะอ่านค่าที่ตำแหน่งนั้น ส่วน set จะเขียนค่าที่ตำแหน่งนั้น คุณสามารถระบุ endianness ได้โดยใช้อาร์กิวเมนต์ที่สองเมื่อต้องจัดการกับข้อมูลที่มีขนาด 16, 32 หรือ 64 บิต ในการใช้งานจริง ควร ระบุ endianness เสมอ ตามข้อกำหนดของข้อมูล

ตัวอย่างต่อไปนี้จะแสดงวิธีเขียนจำนวนเต็ม 32 บิต และเลขทศนิยม 32 บิตในรูปแบบ little endian ลงในบัฟเฟอร์ และอ่านกลับในฟอร์แมตเดียวกัน

 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)
  • โดยการระบุ endianness อย่างชัดเจน คุณจะสามารถแน่ใจได้ว่าข้อมูลเข้ากันได้กับแพลตฟอร์มและข้อกำหนดไบนารีต่าง ๆ

เกี่ยวกับ Endianness (ลำดับไบต์)

โปรโตคอลบางชนิด เช่นที่ใช้ในเครือข่าย จะใช้ big-endian ในขณะที่ CPU หลายรุ่นและฟอร์แมตไฟล์มักจะใช้ little-endian เป็นหลัก ใน DataView หากอาร์กิวเมนต์ที่สองเป็น true จะถือว่าข้อมูลเป็น little endian; หากเป็น false หรือไม่ระบุ จะถือว่าเป็น big endian

ในตัวอย่างต่อไปนี้ คุณจะเห็นว่าอาเรย์ไบต์ในหน่วยความจำเปลี่ยนไปอย่างไรเมื่อเขียนตัวเลขเดียวกันด้วยรูปแบบ big-endian และ 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]
  • การเข้าใจการเปลี่ยนแปลงลำดับไบต์ระหว่าง big endian และ little endian จะช่วยให้ง่ายต่อการวิเคราะห์ข้อมูลการสื่อสารหรือฟอร์แมตไบนารี

การรับและส่งข้อความ (ใช้ TextEncoder และ Decoder ร่วมกัน)

DataView เหมาะสำหรับการอ่านและเขียนตัวเลข แต่ไม่รองรับการจัดการสตริงโดยตรง โดยทั่วไปจะมีการเข้ารหัสสตริงด้วย TextEncoder หรือ TextDecoder (เช่น เป็น 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);
  • โดยการแปลงสตริงเป็นไบนารี และถ้าจำเป็นให้นำความยาวไปไว้ข้างหน้าด้วย คุณจะสามารถจัดเก็บสตริงความยาวผันแปรได้

ตัวอย่างการใช้งาน: การเข้ารหัส/ถอดรหัสฟอร์แมตไบนารีที่กำหนดเอง

ด้านล่างนี้ เรากำหนดรูปแบบข้อความอย่างง่ายที่มีหมายเลขเวอร์ชัน, รหัส, และชื่อ เราดำเนินกระบวนการเข้ารหัสเพื่อแปลงข้อความเป็นข้อมูลไบนารี และกระบวนการถอดรหัสเพื่อกู้คืนวัตถุต้นฉบับจากไบนารี ในรูปแบบข้อความนี้ ไบต์แรกจะจัดเก็บเวอร์ชัน, 4 ไบต์ถัดไปจะจัดเก็บรหัสในรูปแบบ little-endian, ไบต์ต่อไปเป็นความยาวของชื่อ และท้ายสุดเป็นชื่อที่ถูกเข้ารหัสแบบ 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 มีหลายจุดที่ควรพิจารณานอกเหนือจากการอ่านและเขียนค่าธรรมดา เช่น ความปลอดภัย, การใช้ endianness อย่างสม่ำเสมอ, และการจัดการชนิดข้อมูลที่เหมาะสม โดยเฉพาะอย่างยิ่ง เมื่อจัดการกับข้อมูลไบนารีที่รับมาจากแหล่งอื่น หรือข้อมูลจำนวนเต็มขนาดใหญ่ จำเป็นต้องออกแบบโค้ดเพื่อป้องกันการอ่านผิดพลาดและเกินขอบเขตบัฟเฟอร์ ด้านล่างนี้คือข้อควรระวัติที่เป็นประโยชน์สำหรับการใช้งานจริง

  • การตรวจสอบขอบเขต DataView จะขว้างข้อยกเว้นหาก offset หรือขนาดเกินขอบเขตของบัฟเฟอร์ ควรตรวจสอบความยาวข้อมูลทุกครั้งเมื่อทำงานกับข้อมูลไบนารีที่ไม่น่าเชื่อถือ

  • ระบุ Endianness เสมอ ระบุว่าใช้ little endian หรือ big endian อย่างชัดเจนและสม่ำเสมอในโค้ดของคุณทุกครั้ง

  • ความสอดคล้องของชนิดข้อมูล ตัวเลขใน JavaScript เป็นค่าทศนิยม IEEE-754 ขนาด 64 บิต เมธอดเช่น getUint32 จะจัดการข้อมูลได้เหมาะสม แต่ไม่มี getUint64 ดังนั้นต้องจัดการจำนวนเต็ม 64 บิตเป็นพิเศษ

  • การจัดการจำนวนเต็ม 64 บิต สำหรับการจัดการจำนวนเต็ม 64 บิต คุณจำเป็นต้องใช้ BigInt หรือแบ่งค่าออกเป็นสองส่วน (32 บิตสูงและต่ำ) ด้วยตนเอง นี่คือตัวอย่างง่าย ๆ ของการอ่านค่า unsigned integer ขนาด 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 แต่ก็สามารถแปลงระหว่าง ArrayBuffer กับ DataView ได้อย่างง่ายดายเช่นกัน สามารถใช้พร็อพเพอร์ตี้ 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 เป็นกลไกที่ทรงพลังสำหรับการอ่านและเขียนข้อมูลไบนารีอย่างอิสระ โดยมีประโยชน์โดยเฉพาะเมื่อคุณต้องการควบคุม endianness และเข้าถึงข้อมูลตามตำแหน่งต่างๆ ได้อย่างยืดหยุ่น เมื่อใช้ ArrayBuffer, TypedArray และ DataView ร่วมกัน คุณจะสามารถจัดการข้อมูลไบนารีใน TypeScript ได้อย่างยืดหยุ่นและแม่นยำ รองรับงานหลากหลายตั้งแต่การสร้างโปรโตคอลไปจนถึงการวิเคราะห์ไฟล์ โดยการเพิ่มฟังก์ชันการเข้ารหัสสตริงและการจัดการจำนวนเต็ม 64 บิตตามความต้องการ คุณจะสามารถดำเนินการกับข้อมูลไบนารีได้อย่างสมบูรณ์ยิ่งขึ้น

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video