بث التحديثات باستخدام الأحداث التي أرسلها الخادم

تُرسِل الأحداث المُرسَلة من الخادم (SSE) تعديلات تلقائية إلى عميل من خادم، وذلك من خلال اتصال HTTP . بعد إنشاء الاتصال، يمكن للخوادم بدء نقل البيانات.

قد تحتاج إلى استخدام بروتوكول SSE لإرسال إشعارات فورية من تطبيق الويب. ويُرجى العِلم أنّ بروتوكول SSE يُرسِل المعلومات في اتجاه واحد، وبالتالي لن تتلقّى تعديلات من العميل.

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

  • الاستطلاع: يستطلع التطبيق خادمًا بشكل متكرر للحصول على البيانات. وتستخدم الغالبية العظمى من تطبيقات AJAX هذه التقنية. باستخدام بروتوكول HTTP، يدور جلّ عملية جلب data حول تنسيق الطلب والاستجابة. يقدّم العميل طلبًا وينتظر ردّ الخادم بالبيانات. إذا لم يكن هناك أيّ محتوى متاح، يتم عرض ردّ فارغ. تؤدي عمليات الاستطلاع الإضافية إلى زيادة النفقات العامة لبروتوكول HTTP.

  • الاستطلاع الطويل (Hanging GET / COMET): إذا لم يكن لدى الخادم بيانات متوفّرة، يُبقي الخادم الطلب مفتوحًا إلى أن تصبح البيانات الجديدة متاحة. وبالتالي، غالبًا ما يُشار إلى هذه التقنية باسم "طريقة استرداد البيانات المعلّقة". وعندما تصبح المعلومات متوفرة، يستجيب الخادم ويغلق الاتصال وتتكرر العملية. وبالتالي، يستجيب الخادم باستمرار باستخدام بيانات جديدة. لإعداد هذه الطريقة، يستخدم مطوّرو البرامج عادةً عمليات الاختراق مثل إلحاق علامات النصوص البرمجية في إطار iframe "غير محدود".

تم تصميم الأحداث المُرسَلة من الخادم من الألف إلى الياء لتكون فعّالة. عند التواصل مع خدمات SSE، يمكن لخادم دفع البيانات إلى تطبيقك متى شاء، بدون الحاجة إلى تقديم طلب أولي. وبعبارة أخرى، يمكن بث التحديثات من خادم إلى عميل فور حدوثها. تفتح SSE قناة واحدة أحادية الاتجاه بين الخادم والعميل.

يكمن الاختلاف الرئيسي بين الأحداث المُرسَلة من الخادم وطلبات الاستماع الطويلة في أنّ المتصفح يعالج أحداث SSE مباشرةً، وعلى المستخدم فقط الاستماع إلى الرسائل.

الأحداث التي يرسلها الخادم مقابل WebSockets

لماذا تختار الأحداث المُرسَلة من الخادم بدلاً من WebSockets؟ سؤال جيد.

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

ومع ذلك، تحتاج أحيانًا إلى اتّصال أحادي الاتجاه من خادم فقط. على سبيل المثال، عندما يعدّل صديق حالته أو رمز سهم الأسهم أو خلاصات الأخبار أو آليات أخرى لدفع البيانات المبرمَجة. بعبارة أخرى، تعديل على قاعدة بيانات لغة الاستعلامات البنيوية (SQL) على الإنترنت أو على متجر عناصر IndexedDB من جهة العميل إذا كنت بحاجة إلى إرسال البيانات إلى خادم، يمكنك استخدام XMLHttpRequest في أي وقت.

يتم إرسال SSE عبر HTTP. وليس هناك أي بروتوكول خاص أو تنفيذ خادم خاص لبدء العمل. تتطلّب WebSockets اتصالات بدوام كامل وخوادم WebSocket جديدة لمعالجة البروتوكول.

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

إنشاء EventSource باستخدام JavaScript

للاشتراك في بث أحداث، أنشئ عنصرًا من النوع EventSource وأرسِل إليه عنوان URL للبث:

const source = new EventSource('stream.php');

بعد ذلك، عليك إعداد معالِج لحدث message. يمكنك اختياريًا الاستماع إلى open وerror:

source.addEventListener('message', (e) => {
  console.log(e.data);
});

source.addEventListener('open', (e) => {
  // Connection was opened.
});

source.addEventListener('error', (e) => {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});

عند دفع التعديلات من الخادم، يتم تشغيل معالِج onmessage وتتوفّر البيانات الجديدة في سمة e.data. الجزء الرائع هو أنّه عند إغلاق الاتصال، يعيد المتصفح الاتصال تلقائيًا بالمصدر بعد 3 ثوانٍ تقريبًا. يمكن أن تتيح لك عملية تنفيذ الخادم التحكّم في مهلة إعادة الاتصال هذه.

ما مِن إجراءات أخرى مطلوبة. يمكن لعميلك الآن معالجة الأحداث من stream.php.

تنسيق مجموعة بث الحدث

إنّ إرسال بث أحداث من المصدر هو عبارة عن إنشاء ردّ بنص عادي، يتم تقديمه مع text/event-stream Content-Type، يتبع تنسيق SSE. في شكله الأساسي، يجب أن يحتوي الردّ على سطر data:، متبوعًا بسلسلة الرسائل، متبوعةً بحرفَي "\n" لإنهاء البث:

data: My message\n\n

البيانات المتعدّدة الأسطر

إذا كانت رسالتك أطول من ذلك، يمكنك تقسيمها باستخدام أسطر data: متعددة. يتم التعامل مع سطرَين أو أكثر متتاليين يبدأان بـ data: على أنّهما قطعة بيانات واحدة، ما يعني أنّه يتم تشغيل حدث message واحد فقط.

يجب أن ينتهي كل سطر بعلامة "\n" واحدة (باستثناء السطر الأخير الذي يجب أن ينتهي بحرفين). النتيجة التي يتم تمريرها إلى معالِج message هي سلسلة واحدة متسلسلة بواسطة أحرف سطر جديد. على سبيل المثال:

data: first line\n
data: second line\n\n</pre>

يؤدي ذلك إلى إنشاء "السطر الأول\nالسطر الثاني" في e.data. يمكن بعد ذلك استخدام e.data.split('\n').join('') لإعادة إنشاء الرسالة بدون أحرف "\n".

إرسال بيانات JSON

يساعدك استخدام أسطر متعددة في إرسال ملف JSON بدون كسر البنية:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

والرمز البرمجي المحتمل من جهة العميل لمعالجة هذا المصدر:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

ربط رقم تعريف بحدث

يمكنك إرسال معرّف فريد مع حدث بث من خلال تضمين سطر يبدأ برمز id::

id: 12345\n
data: GOOG\n
data: 556\n\n

يتيح ضبط معرّف للمتصفّح تتبُّع آخر حدث تم تشغيله، فإذا تم إنهاء الاتصال بالخادم، يتم ضبط عنوان HTTP خاص (Last-Event-ID) مع الطلب الجديد. يتيح ذلك للمتصفّح تحديد الحدث المناسب لبدء التنفيذ. يحتوي حدث message على السمة e.lastEventId.

التحكّم في مهلة إعادة الاتصال

يحاول المتصفّح إعادة الاتصال بالمصدر بعد 3 ثوانٍ تقريبًا بعد إغلاق كل عملية اتصال. يمكنك تغيير هذه المهلة من خلال تضمين سطر يليه عدد الميلّي ثانية التي يجب الانتظار خلالها قبل محاولة إعادة الاتصال.

يحاول المثال التالي إعادة الاتصال بعد 10 ثوانٍ:

retry: 10000\n
data: hello world\n\n

تحديد اسم الحدث

يمكن لمصدر حدث واحد إنشاء أنواع مختلفة من الأحداث من خلال تضمين اسم حدث. في حال توفّر سطر يبدأ بـ event:، متبوعًا باسم فريد للحدث، يتم ربط الحدث بهذا الاسم. في جهاز العميل، يمكن إعداد أداة معالجة الحدث للاستماع إلى هذا الحدث المحدّد.

على سبيل المثال، تُرسِل مخرجات الخادم التالية ثلاثة أنواع من الأحداث، وهي: حدث "message" العام وحدث "userlogon" وحدث "update":

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

عند إعداد أدوات معالجة الأحداث على العميل:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.msg);
});

source.addEventListener('userlogon', (e) => {
  const data = JSON.parse(e.data);
  console.log(`User login: ${data.username}`);
});

source.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.username} is now ${data.emotion}`);
};

أمثلة على الخوادم

في ما يلي مثال على تنفيذ أساسي لخادم في PHP:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/

function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>

في ما يلي تنفيذ مشابه على Node JS باستخدام معالج Express:

app.get('/events', (req, res) => {
    // Send the SSE header.
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // Sends an event to the client where the data is the current date,
    // then schedules the event to happen again after 5 seconds.
    const sendEvent = () => {
        const data = (new Date()).toLocaleTimeString();
        res.write("data: " + data + '\n\n');
        setTimeout(sendEvent, 5000);
    };

    // Send the initial event immediately.
    sendEvent();
});

sse-node.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
    const source = new EventSource('/events');
    source.onmessage = (e) => {
        const content = document.createElement('div');
        content.textContent = e.data;
        document.body.append(content);
    };
    </script>
  </body>
</html>

إلغاء بث أحداث

وعادةً ما يعيد المتصفّح تلقائيًا الاتصال بمصدر الحدث عند إغلاق الاتصال، ولكن يمكن إلغاء هذا السلوك من العميل أو الخادم.

لإلغاء بث من العميل، يُرجى الاتصال على:

source.close();

لإلغاء بث من الخادم، يجب الردّ بقيمة غير text/event-stream Content-Type أو عرض حالة HTTP غير 200 OK (مثل 404 Not Found).

تمنع كلتا الطريقتَين المتصفّح من إعادة إنشاء الاتصال.

لمحة عن الأمان

تخضع الطلبات التي يتم إنشاؤها بواسطة EventSource لسياسات المصدر نفسه مثل واجهات برمجة التطبيقات الأخرى للشبكة، مثل fetch. إذا كنت بحاجة إلى أن تكون نقطة نهاية ميزة "التشفير من جهة العميل" على خادمك متاحة للوصول إليها من مصادر مختلفة، يمكنك الاطّلاع على كيفية تفعيلها باستخدام مشاركة الموارد المتعدّدة المصادر (CORS).