`DataView` في TypeScript

`DataView` في TypeScript

توضح هذه المقالة DataView في TypeScript۔

سنشرح DataView في TypeScript من الأساسيات إلى الاستخدام العملي خطوة بخطوة۔

YouTube Video

DataView في TypeScript

باستخدام DataView في TypeScript، يمكنك تنفيذ عمليات قراءة وكتابة دقيقة على مستوى البايت على كائن ArrayBuffer۔ DataView مفيد جدًا لمعالجة البيانات الثنائية منخفضة المستوى مثل تطبيق البروتوكولات، وتحليل الملفات الثنائية، وإرسال/استقبال البيانات الثنائية عبر WebSocket۔

المفاهيم الأساسية: الفروقات بين ArrayBuffer وTypedArray وDataView

ArrayBuffer هو هيكل بيانات أساسي يُستخدم لتخزين تسلسل ثابت الطول من البايتات۔ المصفوفات المخصصة مثل Uint8Array هي وجهات (Views) تتعامل مع المخزن المؤقت كمصفوفة من نوع محدد، حيث أن كل عنصر له نوع ثابت۔

من ناحية أخرى، يُعد 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، يمكنك قراءة وكتابة القيم بشكل مرن من خلال تحديد مواضع وأطوال البايتات۔

الطرق الرئيسية لـ DataView

DataView هو واجهة لمعالجة ArrayBuffer على مستوى البايت، ويوفر طرقًا لقراءة وكتابة أنواع مختلفة مثل الأعداد الصحيحة والأعداد العشرية۔

الطرق الرئيسية كالتالي:۔

  • قراءة: getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • كتابة: setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

جميع هذه الطرق تأخذ 'إزاحة البايت' كالمُعامل الأول: تقوم get بقراءة القيمة في ذلك الموقع، وset بكتابة القيمة هناك۔ يمكنك أيضًا تحديد ترتيب البايتات (Endianness) كالمُعامل الثاني عند التعامل مع بيانات 16 أو 32 أو 64 بت۔ عمليًا، من الأفضل دائمًا تحديد نظام ترتيب البايتات بحسب مواصفات البيانات۔

يوضح المثال التالي كيفية كتابة عدد صحيح 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)

بعض البروتوكولات، مثل المستخدمة في الشبكات، تعتمد Big-Endian، بينما تعتمد العديد من وحدات المعالجة المركزية وصيغ الملفات نظام 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 و TextDecoder معًا)

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);
  • من خلال تحويل النصوص إلى ثنائي، وإذا لزم الأمر، إضافة طولها في البداية، يمكنك تخزين نصوص بطول متغير۔

مثال عملي: تشفير/فك تشفير تنسيق ثنائي مُخصص

فيما يلي، نعرّف تنسيق رسالة بسيط يحتوي على رقم الإصدار ومعرّف واسم.۔ ننفذ عملية ترميز لتحويل الرسائل إلى بيانات ثنائية، وعملية فك ترميز لاستعادة الكائن الأصلي من البيانات الثنائية.۔ في تنسيق الرسالة هذا، يخزن البايت الأول رقم الإصدار، وتخزن البايتات الأربعة التالية المعرّف بتنسيق 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، هناك بعض الأمور العملية التي ينبغي أخذها بعين الاعتبار بجانب القراءة والكتابة، مثل السلامة، واستمرار نظام البايتات، والتعامل الصحيح مع الأنواع۔ على وجه الخصوص، عند التعامل مع بيانات ثنائية واردة من مصادر خارجية أو مع الأعداد الكبيرة، من المهم تصميم الكود لمنع القراءة الخاطئة أو تجاوز المخزن المؤقت۔ فيما يلي بعض النقاط المفيدة التي يجب تذكرها للاستخدام العملي.۔

  • التحقق من الحدود DataView يطلق استثناء إذا تجاوز الموضع أو الحجم حدود المخزن المؤقت۔ تحقق دائمًا من الأطوال عند التعامل مع بيانات ثنائية غير موثوقة۔

  • حدد دائمًا ترتيب البايتات (Endianness) حدد دائمًا وبوضوح، وبصورة متسقة، استخدام Little Endian أو Big Endian في الكود الخاص بك۔

  • اتساق الأنواع الأرقام في 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، فإنه من السهل أيضًا التحويل بين 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 آلية قوية لقراءة وكتابة البيانات الثنائية بحرية—وخاصة في الحالات التي تتطلب تحكمًا مرنًا مثل تحديد ترتيب البايتات أو الوصول إلى أي موضع۔ من خلال دمج ArrayBuffer وTypedArray وDataView، يمكنك التعامل مع البيانات الثنائية بمرونة ودقة باستخدام TypeScript، مما يتيح العديد من حالات الاستخدام من تطبيق البروتوكولات إلى تحليل الملفات۔ ومن خلال تضمين ترميز النصوص والتعامل مع الأعداد الصحيحة 64-بت عند الحاجة، يمكنك تحقيق عمليات ثنائية أكثر عملية۔

يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔

YouTube Video