WebSockets والتطبيقات الفورية

واجهة برمجة تطبيقات WebSocket الأصلية في المتصفح

20 دقيقة الدرس 3 من 35

منشئ WebSocket

توفر واجهة برمجة تطبيقات WebSocket الأصلية في المتصفح واجهة بسيطة لإنشاء اتصالات WebSocket. يمكنك إنشاء اتصال باستخدام منشئ WebSocket:

// اتصال WebSocket أساسي const socket = new WebSocket('wss://example.com/socket'); // WebSocket مع بروتوكول فرعي const socketWithProtocol = new WebSocket( 'wss://example.com/socket', 'chat-v1' ); // WebSocket مع بروتوكولات فرعية متعددة (يختار الخادم واحدًا) const socketMultiProtocol = new WebSocket( 'wss://example.com/socket', ['chat-v2', 'chat-v1'] );

معلمات المنشئ:

  • url (مطلوب): عنوان URL لـ WebSocket (ws:// أو wss://)
  • protocols (اختياري): سلسلة أو مصفوفة من أسماء البروتوكولات الفرعية
البروتوكولات الفرعية: تتيح لك البروتوكولات الفرعية تحديد بروتوكولات مخصصة على مستوى التطبيق فوق WebSocket. على سبيل المثال، 'mqtt' أو 'stomp' أو بروتوكولات مخصصة مثل 'chat-v1'. يجب أن يدعم الخادم البروتوكول الفرعي المطلوب لنجاح الاتصال.

أحداث WebSocket

يصدر كائن WebSocket أربعة أحداث رئيسية خلال دورة حياته. يمكنك الاستماع إلى هذه الأحداث باستخدام مستمعي الأحداث أو خصائص معالج الأحداث:

1. حدث onopen

يتم تشغيله عند إنشاء اتصال WebSocket بنجاح:

const socket = new WebSocket('wss://example.com/socket'); // استخدام خاصية معالج الحدث socket.onopen = (event) => { console.log('تم فتح الاتصال'); console.log('البروتوكول:', socket.protocol); console.log('الامتدادات:', socket.extensions); // الآن آمن لإرسال الرسائل socket.send('مرحبًا أيها الخادم!'); }; // استخدام addEventListener (يسمح بمستمعين متعددين) socket.addEventListener('open', (event) => { console.log('WebSocket مفتوح الآن'); });
أفضل ممارسة: انتظر دائمًا حدث 'open' قبل إرسال الرسائل. محاولة الإرسال قبل إنشاء الاتصال ستطرح خطأ.

2. حدث onmessage

يتم تشغيله عند استلام البيانات من الخادم:

socket.onmessage = (event) => { console.log('تم الاستلام:', event.data); // معالجة أنواع البيانات المختلفة if (typeof event.data === 'string') { console.log('رسالة نصية:', event.data); // تحليل JSON إذا كان قابلاً للتطبيق try { const data = JSON.parse(event.data); console.log('JSON محلل:', data); } catch (e) { console.log('ليست بيانات JSON'); } } else if (event.data instanceof Blob) { console.log('رسالة ثنائية (Blob):', event.data); // قراءة بيانات blob event.data.text().then(text => { console.log('محتوى Blob:', text); }); } else if (event.data instanceof ArrayBuffer) { console.log('رسالة ثنائية (ArrayBuffer):', event.data); // معالجة البيانات الثنائية const view = new Uint8Array(event.data); console.log('البايت الأول:', view[0]); } };

خصائص MessageEvent:

  • data: بيانات الرسالة (سلسلة، Blob، أو ArrayBuffer)
  • origin: أصل مرسل الرسالة
  • lastEventId: معرف الحدث الأخير (لـ SSE، لا يُستخدم عادةً في WebSocket)

3. حدث onerror

يتم تشغيله عند حدوث خطأ (فشل الاتصال، خطأ في الشبكة، إلخ):

socket.onerror = (event) => { console.error('خطأ WebSocket:', event); // ملاحظة: كائن Event لا يحتوي على معلومات خطأ مفصلة // لأسباب أمنية. تحقق من وحدة التحكم لمزيد من التفاصيل. console.log('نوع الخطأ:', event.type); console.log('ReadyState:', socket.readyState); };
معلومات خطأ محدودة: لأسباب أمنية، لا يوفر حدث الخطأ معلومات مفصلة حول ما حدث خطأ. ستحتاج إلى استخدام أدوات مطور المتصفح أو تسجيل من جانب الخادم لتشخيص مشاكل الاتصال.

4. حدث onclose

يتم تشغيله عند إغلاق الاتصال (من أي طرف أو بسبب خطأ):

socket.onclose = (event) => { console.log('تم إغلاق الاتصال'); console.log('إغلاق نظيف:', event.wasClean); console.log('كود الإغلاق:', event.code); console.log('سبب الإغلاق:', event.reason); // معالجة أكواد الإغلاق المختلفة switch (event.code) { case 1000: console.log('إغلاق عادي'); break; case 1001: console.log('المغادرة (التنقل في الصفحة أو إيقاف الخادم)'); break; case 1006: console.log('إغلاق غير طبيعي (لم يتم استلام إطار إغلاق)'); // هذا يعني عادة خطأ في الشبكة أو خادم متعطل break; default: console.log('تم الإغلاق بالكود:', event.code); } // تنفيذ منطق إعادة الاتصال setTimeout(() => { console.log('محاولة إعادة الاتصال...'); reconnect(); }, 5000); };

خصائص CloseEvent:

  • wasClean: قيمة منطقية تشير إلى ما إذا تم إغلاق الاتصال بشكل نظيف
  • code: كود حالة الإغلاق الرقمي
  • reason: سلسلة توضح سبب إغلاق الاتصال

طريقة send()

تنقل طريقة send() البيانات إلى الخادم. يمكنها إرسال نص أو Blob أو ArrayBuffer أو بيانات ArrayBufferView:

إرسال البيانات النصية

// إرسال نص عادي socket.send('مرحبًا أيها الخادم!'); // إرسال بيانات JSON (قم بالتحويل إلى سلسلة أولاً) const data = { type: 'message', content: 'مرحبًا!', userId: 123 }; socket.send(JSON.stringify(data));

إرسال البيانات الثنائية

// إرسال ArrayBuffer const buffer = new ArrayBuffer(8); const view = new Uint8Array(buffer); view[0] = 255; view[1] = 128; socket.send(buffer); // إرسال Typed Array (ArrayBufferView) const uint8Array = new Uint8Array([1, 2, 3, 4, 5]); socket.send(uint8Array); // إرسال Blob const blob = new Blob(['Hello World'], { type: 'text/plain' }); socket.send(blob);
تنسيق البيانات الثنائية: بشكل افتراضي، يتم استلام البيانات الثنائية كـ Blob. يمكنك تغيير ذلك باستخدام خاصية binaryType:

socket.binaryType = 'arraybuffer'; // أو 'blob' (افتراضي)

التحقق من حالة المخزن المؤقت

// تحقق من الكمية المخزنة مؤقتًا قبل إرسال بيانات كبيرة function sendLargeData(data) { if (socket.bufferedAmount === 0) { socket.send(data); } else { console.log('المخزن المؤقت ليس فارغًا، في انتظار...'); console.log('البايتات المخزنة مؤقتًا:', socket.bufferedAmount); // انتظر وأعد المحاولة setTimeout(() => sendLargeData(data), 100); } }

خاصية bufferedAmount: تُرجع عدد البايتات الموضوعة في قائمة الانتظار ولكن لم يتم نقلها بعد. مفيدة للتحكم في التدفق عند إرسال كميات كبيرة من البيانات.

خاصية readyState

تشير خاصية readyState إلى الحالة الحالية للاتصال:

// WebSocket.CONNECTING === 0 // WebSocket.OPEN === 1 // WebSocket.CLOSING === 2 // WebSocket.CLOSED === 3 console.log('الحالة الحالية:', socket.readyState); // تحقق من الحالة قبل الإرسال if (socket.readyState === WebSocket.OPEN) { socket.send('رسالة'); } else if (socket.readyState === WebSocket.CONNECTING) { console.log('لا يزال يتصل، انتظر حدث الفتح'); } else { console.log('الاتصال يغلق أو مغلق'); } // دالة مساعدة للتحقق مما إذا كان socket قابلاً للاستخدام function isSocketOpen(socket) { return socket && socket.readyState === WebSocket.OPEN; }
ثوابت الحالة: استخدم الثوابت المسماة (WebSocket.OPEN) بدلاً من الأرقام السحرية (1) لتحسين قابلية قراءة الكود وصيانته.

استلام البيانات الثنائية

قم بتكوين كيفية استلام البيانات الثنائية باستخدام خاصية binaryType:

// تعيين النوع الثنائي إلى ArrayBuffer (أكثر كفاءة للمعالجة) socket.binaryType = 'arraybuffer'; socket.onmessage = (event) => { if (typeof event.data === 'string') { console.log('نص:', event.data); } else if (event.data instanceof ArrayBuffer) { const bytes = new Uint8Array(event.data); console.log('تم استلام بيانات ثنائية:', bytes.length, 'بايت'); // معالجة البيانات الثنائية for (let i = 0; i < bytes.length; i++) { console.log(`البايت ${i}:`, bytes[i]); } } }; // أو استخدم Blob (افتراضي) socket.binaryType = 'blob'; socket.onmessage = async (event) => { if (event.data instanceof Blob) { // قراءة blob كنص const text = await event.data.text(); console.log('Blob كنص:', text); // أو القراءة كـ ArrayBuffer const buffer = await event.data.arrayBuffer(); console.log('Blob كـ buffer:', buffer); } };
اختيار binaryType:
  • 'arraybuffer': أفضل للمعالجة الثنائية المباشرة، حمل أقل
  • 'blob': أفضل للملفات الكبيرة، يمكن بثها، يعمل بشكل جيد مع File API

إغلاق الاتصالات بشكل سليم

أغلق دائمًا اتصالات WebSocket بشكل صحيح باستخدام طريقة close():

// إغلاق بسيط socket.close(); // الإغلاق بكود الحالة socket.close(1000); // إغلاق عادي // الإغلاق بكود الحالة والسبب socket.close(1000, 'تسجيل خروج المستخدم'); // أكواد إغلاق مخصصة على مستوى التطبيق (3000-4999) socket.close(3000, 'انتهت صلاحية الجلسة'); socket.close(4001, 'رمز مصادقة غير صالح');

أكواد الإغلاق الصالحة:

  • 1000-1999: محجوز لبروتوكول WebSocket (استخدم 1000 للإغلاق العادي)
  • 3000-3999: محجوز للمكتبات والأطر
  • 4000-4999: متاح لاستخدام التطبيق
تجنب الإغلاق المفاجئ: لا تضع socket فقط على null أو تنتقل بعيدًا دون استدعاء close(). هذا يخلق اتصالات "نصف مفتوحة" على الخادم تهدر الموارد حتى تنتهي مهلتها.

مثال WebSocket كامل

إليك مثال كامل ينفذ جميع ميزات WebSocket مع معالجة الأخطاء وإعادة الاتصال:

class WebSocketClient { constructor(url) { this.url = url; this.socket = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.connect(); } connect() { try { this.socket = new WebSocket(this.url); this.socket.binaryType = 'arraybuffer'; this.socket.onopen = (event) => { console.log('متصل بخادم WebSocket'); this.reconnectAttempts = 0; // إعادة تعيين عند الاتصال الناجح this.onOpen(event); }; this.socket.onmessage = (event) => { this.onMessage(event); }; this.socket.onerror = (event) => { console.error('خطأ WebSocket:', event); this.onError(event); }; this.socket.onclose = (event) => { console.log('تم إغلاق الاتصال:', event.code, event.reason); this.onClose(event); // محاولة إعادة الاتصال if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnect(); } else { console.error('تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال'); } }; } catch (error) { console.error('فشل إنشاء WebSocket:', error); } } reconnect() { this.reconnectAttempts++; const delay = this.reconnectDelay * this.reconnectAttempts; console.log(`إعادة الاتصال في ${delay}ms (محاولة ${this.reconnectAttempts})`); setTimeout(() => { this.connect(); }, delay); } send(data) { if (this.socket && this.socket.readyState === WebSocket.OPEN) { if (typeof data === 'object') { this.socket.send(JSON.stringify(data)); } else { this.socket.send(data); } } else { console.error('Socket ليس مفتوحًا. ReadyState:', this.socket?.readyState); } } close(code = 1000, reason = 'أغلق العميل الاتصال') { if (this.socket) { this.socket.close(code, reason); } } // تجاوز هذه الطرق في تنفيذك onOpen(event) { // معالجة فتح الاتصال } onMessage(event) { // معالجة الرسائل الواردة console.log('تم الاستلام:', event.data); } onError(event) { // معالجة الأخطاء } onClose(event) { // معالجة إغلاق الاتصال } } // الاستخدام const client = new WebSocketClient('wss://example.com/socket'); // تجاوز المعالجات client.onMessage = (event) => { const data = JSON.parse(event.data); console.log('تم استلام الرسالة:', data); }; // إرسال الرسائل setTimeout(() => { client.send({ type: 'chat', message: 'مرحبًا!' }); }, 1000);
تمرين: قم ببناء عميل دردشة بسيط باستخدام WebSocket API الأصلي:
  • اتصل بخادم WebSocket صدى (wss://echo.websocket.org/)
  • أنشئ حقل إدخال وزر إرسال
  • اعرض الرسائل المرسلة والمستلمة في قائمة
  • اعرض حالة الاتصال (يتصل، مفتوح، مغلق)
  • نفذ إعادة الاتصال التلقائي عند قطع الاتصال

معلمات URL لـ WebSocket

قم بتمرير رموز المصادقة أو معلمات أخرى عبر سلاسل استعلام URL:

// إضافة معلمات الاستعلام const token = 'abc123xyz'; const userId = 42; const url = `wss://example.com/socket?token=${token}&userId=${userId}`; const socket = new WebSocket(url); // يمكن للخادم تحليل معلمات URL للمصادقة/تحديد العميل
اعتبار أمني: تظهر معلمات URL في سجلات الخادم وتاريخ المتصفح. للبيانات الحساسة مثل رموز المصادقة، ضع في اعتبارك إرسالها في الرسالة الأولى بعد الاتصال بدلاً من ذلك، أو استخدم رؤوسًا مخصصة أثناء المصافحة (يتطلب دعم الخادم).

الملخص

توفر واجهة برمجة تطبيقات WebSocket الأصلية واجهة مباشرة للتواصل الثنائي الاتجاه في الوقت الفعلي. تشمل المفاهيم الرئيسية منشئ WebSocket، وأربعة أحداث دورة الحياة (فتح، رسالة، خطأ، إغلاق)، وطريقة send() لنقل البيانات، وreadyState لمراقبة حالة الاتصال، وطريقة close() للانفصال السليم. فهم معالجة البيانات الثنائية وتنفيذ منطق إعادة الاتصال الصحيح ضروري لتطبيقات الإنتاج. في الدرس التالي، سنستكشف بناء خادم WebSocket مع Node.js.