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

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

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

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

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

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

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

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

أحداث Server-sent events في مقابل 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 ثوانٍ تقريبًا بعد إغلاق كل عملية اتصال. يمكنك تغيير هذه المهلة من خلال تضمين سطر يليه retry: ثم عدد الميلّي ثانية التي يجب الانتظار خلالها قبل محاولة إعادة الاتصال.

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

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

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

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

على سبيل المثال، تُرسِل مخرجات الخادم التالية ثلاثة أنواع من الأحداث، وهي: حدث "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).