كيفية استخدام WebSockets في Node.js لإنشاء تطبيقات فورية

توضيح هذا البرنامج كيفية استخدام WebSockets في Node.js للتواصل التفاعلي ثنائي الاتجاه بين متصفح وخادم. هذه التقنية مهمة للتطبيقات السريعة وفي الوقت الحقيقي مثل لوحات المراقبة وتطبيقات المحادثة وألعاب اللاعبين المتعددين.

Table of Contents

يعتمد الويب على رسائل HTTP التي تتضمن طلبًا وردود الفعل. يقوم متصفحك بطلب عنوان URL ويستجيب الخادم ببيانات. قد يؤدي ذلك إلى طلبات متصفح إضافية وردود فعل الخادم للصور والـ CSS وجافاسكريبت وغيرها ولكن الخادم لا يستطيع إرسال بيانات عشوائيًا إلى المتصفح.

تستخدم تقنيات Ajax المتعلقة بالطلبات الطويلة المتعددة لجعل تطبيقات الويب تبدو كما لو أنها تتحدث في الوقت الفعلي، لكن هذه العملية محدودة جدًا بالنسبة للتطبيقات الحقيقية في الوقت الفعلي. ستكون فضيلة التصحيح كل ثانية فعالة في بعض الأوقات وبطيئة جدًا في أوقات أخرى.

بعد الاتصال الأولي من المتصفح، أحداث إرسال الخادم هي تطبيق قياسي (متدفق) لردود HTTP التي يمكن أن ترسل رسائل من الخادم في أي وقت. ومع ذلك، يكون القناة أحادية الاتجاه ولا يمكن للمتصفح إرسال رسائل إلى الخلف. للتواصل ثنائي الاتجاه السريع الحقيقي، تحتاج إلى ويبسوككتس.

نظرة عامة على ويبسوككتس

يشير المصطلح WebSocket إلى بروتوكول اتصال TCP عبر ws:// أو الآمن والمشفر wss://. إنه مختلف عن HTTP، على الرغم من أنه يمكن تشغيله على المنافذ 80 أو 443 لضمان عمله في الأماكن التي تمنع المرور غير الويب. معظم المتصفحات التي صدرت منذ عام 2012 تدعم بروتوكول WebSocket.

في تطبيق ويب حقيقي في الوقت الفعلي، يجب أن يكون لديك خادم ويب على الأقل لتوفير محتوى الويب (HTML وCSS وJavaScript والصور وما إلى ذلك) وخادم ويبسوككت آخر للتعامل مع التواصل ثنائي الاتجاه.

المتصفح لا يزال يقوم بطلب WebSocket ابتدائي إلى خادم، مما يفتح قناة الاتصال. بعد ذلك، يمكن للمتصفح أو الخادم أن يرسلان رسالة عبر تلك القناة، مما يثير حدثًا على الجهاز الآخر.

التواصل مع متصفحات أخرى متصلة

بعد الطلب الأولي، يمكن للمتصفح إرسال واستقبال رسائل إلى/من خادم WebSocket. يمكن لخادم WebSocket إرسال واستقبال رسائل إلى/من أي من متصفحات العملاء المتصلة به.

الاتصال المباشر بين المتصفحات ليس ممكنًا. لا يمكن لـ BrowserA إرسال رسالة مباشرة إلى BrowserB حتى عندما يعملان على نفس الجهاز ومتصلان بنفس خادم WebSocket! يمكن لـ BrowserA إرسال رسالة إلى الخادم فقط والأمل أن يتم توجيهها إلى المتصفحات الأخرى حسب الضرورة.

دعم خادم WebSocket

لا يوجد حاليًا دعم WebSocket أصلي في Node.js، على الرغم من إشاعات بأنه سيأتي قريبًا! في هذه المقالة، أستخدم وحدة ثالثة ws، ولكن هناك عشرات الآخرين.

يتوفر دعم WebSocket مدمجًا في محركات تشغيل JavaScript Deno وBun.

مكتبات WebSocket متاحة لأنظمة التشغيل بما في ذلك PHP, Python, و Ruby. خيارات SaaS الخارجية مثل Pusher و PubNub توفر أيضا خدمات WebSocket مستضافة.

توضيح WebSockets بسرعة

تطبيقات الدردشة هي مرحبا بكم في العالم! لتوضيحات WebSocket، لذا أعتذر لكم على:

  1. عدم الإبداع. ومع ذلك، تطبيقات الدردشة عبارة عن طريقة رائعة لشرح المفاهيم.

  2. عدم القدرة على تقديم حل تشغيلي كامل على الإنترنت. أفضل عدم الاضطرار لمراقبة وتحرير تيار من الرسائل المجهولة!

استنساخ أو تنزيل مستودع node-wschat من GitHub:

git clone https://github.com/craigbuckler/node-wschat

تثبيت تبعيات Node.js:

cd node-wschat
npm install

بدء تشغيل تطبيق الدردشة:

npm start

افتح http://localhost:3000/ في عدد من المتصفحات أو الأوازير (يمكنك أيضًا تحديد اسم الدردشة الخاص بك في سلسلة الاستعلام — مثل http://localhost:3000/?Craig). اكتب شيئًا في نافذة واحدة واضغط على SEND أو اضغط على Enter سترى أنه يظهر في جميع المتصفحات المتصلة.

نظرة عامة على رمز Node.js

ملف الدخول index.js لتطبيق Node.js يبدأ خدمتين:

  1. تطبيق Express يعمل على http://localhost:3000/ مع قالب EJS لتقديم صفحة واحدة بأكواد HTML و CSS و JavaScript على الجانب العميل. تستخدم جافاسكربت المتصفح API WebSocket لإجراء الاتصال الأولي ثم إرسال واستقبال الرسائل.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // خادم WebSocket
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // اتصال العميل
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // تلقي الرسالة
      socket.on('message', (msg, binary) => {
    
        // بث لجميع العملاء
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // مغلق
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

مكتبة Node.js ws:

  • تثار حدث "connection" عندما يرغب متصفح في الاتصال. تتلقى وظيفة معالج الحدث كائن socket يُستخدم للتواصل مع هذا الجهاز الفردي. يجب الاحتفاظ به طوال فترة الاتصال.

  • تثار حدث socket "message" عندما يرسل متصفح رسالة. تقوم وظيفة معالج الحدث ببث الرسالة مرة أخرى إلى كل متصفح متصل (بما في ذلك المتصفح الذي أرسله).

  • تثار حدث socket "close" عندما ينفصل المتصفح – عادة عند إغلاق التبويب أو تحديثه.

عرض سريع لرمز JavaScript على العميل

ملف static/main.js للتطبيق يعمل دالة wsInit() ويمرر عنوان خادم WebSocket (مجموعة أحرف المجال للصفحة مضافًا إليها قيمة المنفذ المحددة في قالب الصفحة التي تم إنشاؤها بواسطة ال HTML):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// التعامل مع اتصال WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // الاتصال بالخادم
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

يتم تشغيل حدث open عندما يقوم المتصفح بالاتصال بخادم WebSocket. ترسل دالة معالج الحدث رسالة دخل غرفة المحادثة عن طريق استدعاء sendMessage():

// إرسال الرسالة
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

تقوم دالة sendMessage() بجلب اسم المستخدم والرسالة من نموذج HTML، على الرغم من أن الرسالة يمكن استبدالها بأي setMsg المُمرر. تتم تحويل القيم إلى كائن JSON وتُرسل إلى خادم WebSocket باستخدام طريقة ws.send().

يتلقى خادم WebSocket الرسالة الواردة مما يثير معالج "message" (انظر أعلاه) ويُشاركها مجددًا مع جميع المتصفحات. يثير هذا حدث "message" على كل عميل:

// تلقي الرسالة
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

يتلقى المعالج البيانات التي تم نقلها التي تحتوي عليها خاصية .data لكائن الحدث. تتم تحليلها إلى كائن JavaScript وتحديث نافذة الدردشة.

أخيرًا، يتم إرسال رسائل جديدة باستخدام دالة sendMessage() في كل مرة يتم فيها تشغيل معالج "submit" للنموذج:

// تسجيل الإرسال
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

التعامل مع الأخطاء

يتم تشغيل حدث "error" عندما يفشل الاتصال بواسطة WebSocket. يمكن التعامل مع هذا على الخادم:

// التعامل مع خطأ WebSocket على الخادم
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

و/أو العميل:

// التعامل مع خطأ WebSocket على العميل
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

يمكن للعميل فقط إعادة إنشاء الاتصال عن طريق تشغيل مُنشئ new WebSocket() مرة أخرى.

إغلاق الاتصالات

يمكن لأي من الأجهزة إغلاق WebSocket في أي وقت باستخدام طريقة .close() للاتصال. يمكنك إعطاء عدد صحيح code وسلسلة reason (حتى 123 بايت) كحجج اختيارية، والتي تُرسل إلى الجهاز الآخر قبل أن ينفصل.

WebSocket المتقدمة

إدارة WebSockets سهلة في Node.js: جهاز واحد يرسل رسالة باستخدام طريقة .send()، والتي تُحدث حدث "message" على الآخر. يمكن أن يكون إنشاء والاستجابة لهذه الرسائل من قبل كل جهاز أكثر تحديًا. تصف الأقسام التالية القضايا التي قد تحتاج إلى التفكير فيها.

أمان WebSocket

لا يتعامل بروتوكول WebSocket مع التصريف أو التوثيق. لا يمكنك ضمان أن الطلب الوارد للاتصال يأتي من متصفح أو مستخدم تم تسجيل الدخول في تطبيقك الويب – خاصة عندما يمكن أن تكون الويب وخوادم WebSocket على أجهزة مختلفة. يتلقى الاتصال الأولي رأس HTTP يحتوي على ملفات تعريف الارتباط والخادم Origin، ولكن من الممكن تزييف هذه القيم.

تضمن التقنية التالية تقييد اتصالات WebSocket للمستخدمين المصرح لهم:

  1. قبل إرسال طلب WebSocket الأولي، يتواصل المتصفح مع خادم الويب HTTP (ربما باستخدام Ajax).

  2. يتحقق الخادم من بيانات المستخدم الشخصية ويرسل تذكرة تفعيل جديدة. عادةً ما تشير التذكرة إلى سجل في قاعدة البيانات يحتوي على معرف المستخدم، عنوان IP، وقت الطلب، وزمن انتهاء الجلسة، وأي بيانات أخرى مطلوبة.

  3. يمرر المتصفح التذكرة لخادم WebSocket في المعاينة الأولية.

  4. يتحقق خادم WebSocket من صحة التذكرة ويتحقق من عوامل مثل عنوان IP، وزمن انتهاء الصلاحية، إلخ قبل السماح بالاتصال. ينفذ طريقة .close() WebSocket عندما تكون التذكرة غير صالحة.

  5. قد يحتاج خادم WebSocket إلى التحقق من سجل قاعدة البيانات من وقت لآخر للتأكد من صحة الجلسة الخاصة بالمستخدم.

من المهم، تحقق دائمًا من البيانات الواردة:

  • كما هو الحال مع HTTP، يعرض خادم WebSocket لهجوم SQL injection وغيرها.

  • يجب ألا يقوم العميل بحقن القيم الأولية في DOM أو تقييم أكواد JavaScript.

الفصل مقابل حالات متعددة من خادم WebSocket

خذ لعبة متعدد اللاعبين على الإنترنت كمثال. تتمتع اللعبة بالعديد من الأكوان التي تلعب حالات منفصلة من اللعبة: universeA، universeB، و universeC. يتصل اللاعب بكون واحد:

  • universeA: المشتركون player1، player2، و player3
  • universeB: المشترك player99

يمكنك تنفيذ ما يلي:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. استخدام خادم WebSocket واحد لجميع كون الألعاب.

    هذا يستخدم موارد أقل ويكون أسهل في الإدارة، لكن خادم WebSocket يجب أن يسجل أي كون ينضم كل لاعب. عندما player1 يقوم بعمل، يجب نشره إلى player2 و player3 ولكن ليس player99.

خوادم WebSocket متعددة

تستطيع التطبيق المبيت المثالي التعامل مع مئات المستخدمين في وقت واحد، لكنه سيتعطل عندما يرتفع الشعبية واستخدام الذاكرة عن العتبات الحرجة. في النهاية ستحتاج إلى التوسع أفقيًا عن طريق إضافة المزيد من الخوادم.

يمكن لكل خادم WebSocket إدارة العملاء المتصلين به فقط. لا يمكن بث رسالة مرسلة من متصفح إلى serverX إلى أولئك المتصلين بـ serverY. قد يصبح ضروريًا تنفيذ نظم النشر والاشتراك (pub-sub) في الخلفية. على سبيل المثال:

  1. خادم WebSocket serverX يرغب في إرسال رسالة إلى جميع العملاء. ينشر الرسالة على نظام النشر والاشتراك.

  2. تتلقى جميع خوادم WebSocket المشتركة في نظام النشر والاشتراك حدث رسالة جديدة (بما في ذلك serverX). يمكن لكل منها معالجة الرسالة وبثها إلى عملائهم المتصلين.

كفاءة بث WebSocket

تتبع الاتصال عبر WebSocket سرعة، لكن الخادم يجب أن يدير جميع العملاء المتصلين. يجب أن تأخذ بعين الاعتبار الآليات وكفاءة الرسائل، خاصة عند بناء ألعاب العدوان المتعارف عليه متعددة اللاعبين:

  • كيف يمكنك sychronize إجراءات لاعب عبر جميع أجهزة العميل؟

  • إذا كان player1 في مكان مختلف عن player2، هل من الضروري إرسال معلومات player2 حول الإجراءات التي لا يمكن رؤيتها؟

  • كيف تتعامل مع تأخر الشبكة – أو تأخر الاتصال؟ هل يمكن لشخص لديه آلة سريعة واتصال له أي ميزة غير عادلة؟

يجب أن تتنازل عن بعض الأشياء في الألعاب السريعة. فكر في الأمر على أنك تلعب اللعبة على جهازك المحلي ولكن بعض الكائنات تتأثر بأنشطة الآخرين. بدلاً من إرسال الموقع الدقيق لكل كائن في كل الأوقات، غالباً ما ترسل الألعاب رسائل أبسط وأقل تكراراً. على سبيل المثال:

  • objectX ظهر في النقطةX
  • objectY لديها اتجاه وسرعة جديدة
  • objectZ تم تدميرها

يمكن لكل لعبة العميل ملء الفجوات. عندما ينفجر objectZ، لن يكون الأمر مهمًا إذا كان الانفجار يبدو مختلفًا على كل جهاز.

الخاتمة

Node.js يجعل من السهل التعامل مع WebSockets. لا يجعل بالضرورة التطبيقات الفورية أسهل في التصميم أو البرمجة، لكن التكنولوجيا لن تعيقك!

العيوب الرئيسية:

  • تتطلب WebSockets مثيلًا خاصًا للخادم. يمكن معالجة طلبات Ajax Fetch() و الأحداث المرسلة من الخادم بواسطة الويب خادم الذي تعمل به بالفعل.

  • يتطلب خوادم WebSocket فحوصات أمانيها وفحص التوثيق الخاصة بها.

  • يجب إعادة إقامة اتصالات WebSocket المتوقفة بشكل يدوي.

لكن لا تدع ذلك يثنيك!

الأسئلة الشائعة (FAQs) حول التطبيقات الفورية باستخدام WebSockets والأحداث المرسلة من الخادم

كيف تختلف WebSockets عن HTTP من حيث الأداء والوظائف؟

WebSockets توفران قناة اتصال مزدوجة الاتجاه بأكملها عبر اتصال TCP واحد، مما يعني أن البيانات يمكن إرسالها واستقبالها بشكل متزامن. هذا تحسين كبير على أنظمة HTTP، حيث يتطلب كل طلب اتصالًا جديدًا. تسمح WebSockets أيضًا بنقل البيانات في الوقت الفعلي، مما يجعلها مثالية للتطبيقات التي تحتاج إلى تحديثات فورية، مثل تطبيقات المحادثة أو تحديثات الرياضة الحية. من ناحية أخرى، تكون HTTP غير مستمرة وكل زوج من الطلب والاستجابة مستقل، والتي قد تكون أكثر ملاءمة للتطبيقات حيث لا تكون التحديثات في الوقت الفعلي ضرورية.

هل يمكنك شرح دورة الحياة لاتصال WebSocket؟

دورة الحياة لاتصال WebSocket تبدأ بصفحة تحول اتصال HTTP إلى اتصال WebSocket. بمجرد إقامة الاتصال، يمكن إرسال واستلام البيانات بين العميل والخادم حتى يقرر أحد الطرفين إغلاق الاتصال. يمكن إغلاق الاتصال عن طريق إرسال العميل أو الخادم لإغلاق الإطار، تليها الطرف الآخر يعترف بإغلاق الإطار.

كيف يمكنني تنفيذ WebSockets في تطبيق Android؟

تنفيذ WebSockets في تطبيق Android يتضمن إنشاء عميل WebSocket يمكنه الاتصال بخادم WebSocket. يمكن القيام بذلك باستخدام مكتبات مثل OkHttp أو Scarlet. بمجرد إعداد العميل، يمكنك فتح اتصال بالخادم، إرسال واستقبال الرسائل، والتعامل مع أحداث مختلفة مثل فتح الاتصال، استقبال الرسائل، وإغلاق الاتصال.

ما هي الأحداث المُرسلة من الخادم وكيف تقارن مع WebSockets؟

تعتبر حدث Server-Sent Events (SSE) من المعايير التي تسمح لخادم بإرسال تحديثات إلى عميل عبر HTTP. على عكس WebSockets، تكون الأحداث SSE فردية، مما يعني أنها تسمح فقط بإرسال البيانات من الخادم إلى العميل. هذا يجعلها أقل ملاءمة للتطبيقات التي تتطلب الاتصال الثنائي، لكنها يمكن أن تكون حلاً أبسط وأكثر كفاءة للتطبيقات التي تحتاج فقط إلى تحديثات من الخادم.

ما هي بعض الاستخدامات الشائعة لـ WebSockets و Server-Sent Events؟

يُستخدم WebSockets بشكل شائع في التطبيقات التي تتطلب الاتصال الحقيقي الثنائي، مثل تطبيقات المحادثة، الألعاب متعددة اللاعبين، والأدوات التعاونية. من ناحية أخرى، غالباً ما يُستخدم Server-Sent Events في التطبيقات التي تحتاج إلى تحديثات فورية من الخادم، مثل تحديثات الأخبار المباشرة، تحديثات أسعار الأسهم، أو تقارير التقدم لأعمال تشغيل طويلة.

كيف يمكنني التعامل مع اتصالات WebSocket في تطبيق Spring Boot؟

يوفر Spring Boot دعمًا للاتصالات WebSocket من خلال وحدة Spring WebSocket. يمكنك استخدام تعليمة @EnableWebSocket لتمكين دعم WebSocket، ثم تحديد WebSocketHandler لمعالجة دورة الاتصال ومعالجة الرسائل. يمكنك أيضًا استخدام SimpMessagingTemplate لإرسال رسائل إلى العملاء المتصلين.

ما هي عوامل الأمان المتعلقة باستخدام WebSockets؟

مثل أي تكنولوجيا ويب أخرى، يمكن أن تكون WebSockets عرضة لمختلف التهديدات الأمنية، مثل اغتيال WebSocket المتعلق بالمواقع المتعددة (CSWSH) والهجوم بتنكر الخدمة (DoS). لتخفيف هذه المخاطر، يجب دائمًا استخدام اتصالات WebSocket آمنة (wss://) والتحقق وتطهير جميع البيانات الواردة. يجب عليك أيضًا التفكير في استخدام آليات التوثيق والترخيص للسيطرة على الوصول إلى خادم WebSocket الخاص بك.

هل يمكنني استخدام WebSockets مع واجهة برمجة تطبيقات REST؟

نعم، يمكنك استخدام WebSockets بالتزامن مع واجهة برمجة تطبيقات REST. بينما تكون واجهات برمجة تطبيقات REST مثالية للاتصالات اللازمة للاستجابة اللازمة في الاتصالات، يمكن استخدام WebSockets للاتصالات الحقيقية المباشرة. يمكن أن يكون هذا مفيدًا بشكل خاص في التطبيقات التي تتطلب تحديثات فورية، مثل تطبيقات الدردشة أو تحديثات الرياضة المباشرة.

كيف يمكنني اختبار خادم WebSocket؟

هناك عدة أدوات متاحة لاختبار خادم WebSocket، مثل اختبار الصدى في WebSocket.org، أو Postman. تسمح لك هذه الأدوات بفتح اتصال WebSocket إلى خادم، وإرسال رسائل، واستلام الردود. يمكنك أيضًا كتابة اختبارات تلقائية لخادم WebSocket الخاص بك باستخدام مكتبات مثل Jest أو Mocha.

ما هي قيود WebSockets والأحداث الموجهة من الخادم؟

بينما توفر WebSockets والأحداث الموجهة من الخادم قدرات قوية للاتصالات في الوقت الحقيقي، لها أيضًا قيودها. على سبيل المثال، لا تدعم كل المتصفحات والشبكات هذه التقنيات، ويمكن أن تستهلك كمية كبيرة من الموارد إذا لم يتم إدارتها بشكل صحيح. علاوة على ذلك، يمكن أن تكون أكثر تعقيدًا في التنفيذ والإدارة مقارنة بالاتصالات التقليدية HTTP.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/