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

أحداث الخادم المرسلة (SSE)

16 دقيقة الدرس 18 من 35

أحداث الخادم المرسلة (SSE)

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

SSE مقابل WebSockets

فهم متى تستخدم كل تقنية:

الميزة SSE WebSockets
الاتجاه من الخادم إلى العميل فقط ثنائي الاتجاه
البروتوكول HTTP بروتوكول WebSocket
إعادة الاتصال تلقائي يدوي
تنسيق البيانات نص فقط (UTF-8) نص وثنائي
دعم المتصفح جميع المتصفحات الحديثة جميع المتصفحات الحديثة
التعقيد بسيط أكثر تعقيدًا
متى تستخدم SSE: موجزات الأخبار، مؤشرات الأسهم، الإشعارات، النتائج المباشرة، لوحات مراقبة الخادم - أي سيناريو يحتاج فيه الخادم إلى دفع التحديثات ولكن العملاء لا يحتاجون إلى إرسال رسائل متكررة.

واجهة EventSource API

واجهة برمجة التطبيقات من جانب العميل لـ SSE واضحة ومباشرة:

<!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <title>مثال SSE</title> </head> <body> <div id="notifications"></div> <script> // إنشاء اتصال EventSource const eventSource = new EventSource('/api/events'); // الاستماع للرسائل eventSource.onmessage = (event) => { console.log('رسالة جديدة:', event.data); displayNotification(event.data); }; // الاستماع لفتح الاتصال eventSource.onopen = () => { console.log('تم فتح الاتصال'); }; // الاستماع للأخطاء eventSource.onerror = (error) => { console.error('خطأ EventSource:', error); if (eventSource.readyState === EventSource.CLOSED) { console.log('تم إغلاق الاتصال'); } }; function displayNotification(message) { const div = document.createElement('div'); div.textContent = message; document.getElementById('notifications').appendChild(div); } // إغلاق الاتصال عند الحاجة // eventSource.close(); </script> </body> </html>

إنشاء نقطة نهاية SSE في Express

التنفيذ من جانب الخادم مع Node.js و Express:

const express = require('express'); const app = express(); // نقطة نهاية SSE app.get('/api/events', (req, res) => { // تعيين رؤوس لـ SSE res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // اختياري: تمكين CORS res.setHeader('Access-Control-Allow-Origin', '*'); // إرسال تعليق أولي (يساعد في إبقاء الاتصال حيًا) res.write(':ok\n\n'); // إرسال رسالة كل 5 ثوانٍ const intervalId = setInterval(() => { const data = { timestamp: new Date().toISOString(), message: 'مرحبا من الخادم' }; // التنسيق: data: JSON\n\n res.write(`data: ${JSON.stringify(data)}\n\n`); }, 5000); // التنظيف عند انفصال العميل req.on('close', () => { clearInterval(intervalId); console.log('تم فصل العميل'); }); }); app.listen(3000, () => { console.log('خادم SSE يعمل على المنفذ 3000'); });
ملاحظة: تنسيق رسالة SSE صارم: data: message\n\n. السطر الجديد المزدوج (\n\n) يشير إلى نهاية الرسالة.

إرسال الأحداث مع أنواع الأحداث

يدعم SSE أنواع أحداث مخصصة لفئات رسائل مختلفة:

// جانب الخادم: إرسال أنواع أحداث مختلفة app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // إرسال حدث إشعار function sendNotification(message) { res.write(`event: notification\n`); res.write(`data: ${JSON.stringify(message)}\n\n`); } // إرسال حدث تحديث function sendUpdate(data) { res.write(`event: update\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); } // إرسال حدث تنبيه function sendAlert(alert) { res.write(`event: alert\n`); res.write(`data: ${JSON.stringify(alert)}\n\n`); } // مثال: إرسال أحداث مختلفة setInterval(() => { sendNotification({ text: 'تعليق جديد على منشورك' }); }, 10000); setInterval(() => { sendUpdate({ users: 42, requests: 1523 }); }, 5000); req.on('close', () => { console.log('تم فصل العميل'); }); });
// جانب العميل: الاستماع لأنواع أحداث محددة const eventSource = new EventSource('/api/events'); // الاستماع لأحداث الإشعارات eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); console.log('إشعار:', data.text); showNotification(data.text); }); // الاستماع لأحداث التحديث eventSource.addEventListener('update', (event) => { const data = JSON.parse(event.data); console.log('تحديث:', data); updateDashboard(data); }); // الاستماع لأحداث التنبيه eventSource.addEventListener('alert', (event) => { const data = JSON.parse(event.data); console.log('تنبيه:', data); showAlert(data); }); // معالج الرسائل العام (للأحداث بدون نوع) eventSource.onmessage = (event) => { console.log('رسالة عامة:', event.data); };

إعادة الاتصال التلقائية

يعيد SSE الاتصال تلقائيًا عند انقطاع الاتصال:

// الخادم: إرسال فاصل إعادة المحاولة (بالمللي ثانية) app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // تعيين فاصل إعادة المحاولة إلى 3 ثوانٍ res.write('retry: 3000\n\n'); // إرسال الأحداث... });
// العميل: معالجة إعادة الاتصال const eventSource = new EventSource('/api/events'); let reconnectAttempts = 0; const maxReconnectAttempts = 5; eventSource.onerror = (error) => { if (eventSource.readyState === EventSource.CONNECTING) { reconnectAttempts++; console.log(`إعادة الاتصال... (محاولة ${reconnectAttempts})`); if (reconnectAttempts >= maxReconnectAttempts) { console.error('تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال'); eventSource.close(); // إظهار إشعار المستخدم alert('فقد الاتصال بالخادم. يرجى التحديث.'); } } else if (eventSource.readyState === EventSource.CLOSED) { console.log('تم إغلاق الاتصال'); } }; eventSource.onopen = () => { reconnectAttempts = 0; console.log('تم (إعادة) إنشاء الاتصال'); };

معرف الحدث الأخير للاستئناف

استخدم معرفات الأحداث للاستئناف من آخر حدث تم استلامه بعد إعادة الاتصال:

// الخادم: إرسال أحداث بمعرفات let eventId = 0; app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // الحصول على معرف الحدث الأخير من العميل (إذا كان يعيد الاتصال) const lastEventId = req.headers['last-event-id']; console.log('معرف الحدث الأخير المستلم:', lastEventId); // إرسال أحداث بمعرفات const intervalId = setInterval(() => { eventId++; res.write(`id: ${eventId}\n`); res.write(`data: ${JSON.stringify({ id: eventId, message: 'الحدث ' + eventId })}\n\n`); }, 2000); req.on('close', () => { clearInterval(intervalId); }); });
// العميل: يرسل EventSource تلقائيًا معرف الحدث الأخير عند إعادة الاتصال const eventSource = new EventSource('/api/events'); eventSource.onmessage = (event) => { console.log('معرف الحدث:', event.lastEventId); console.log('البيانات:', event.data); // تخزين معرف الحدث الأخير إذا لزم الأمر localStorage.setItem('lastEventId', event.lastEventId); };
أفضل ممارسة: أرسل دائمًا معرفات الأحداث لتدفقات البيانات الهامة. يتيح ذلك للعملاء الاستئناف من آخر حدث تم استلامه بعد الانقطاع، مما يمنع فقدان البيانات.

حالات استخدام SSE

السيناريوهات المثالية لأحداث الخادم المرسلة:

  • الإشعارات في الوقت الفعلي: إشعارات المستخدم، التنبيهات، رسائل النظام
  • الموجزات المباشرة: تحديثات الأخبار، موجزات وسائل التواصل الاجتماعي، تدفقات النشاط
  • لوحات المراقبة: مقاييس الخادم، سجلات التطبيق، التحليلات
  • مؤشرات الأسهم: أسعار الأسهم المباشرة، أسعار العملات الرقمية
  • نتائج الرياضة: تحديثات المباراة المباشرة، تغييرات النتيجة
  • تتبع التقدم: تحميل الملفات، المهام طويلة الأمد
  • تدفقات بيانات إنترنت الأشياء: قراءات المستشعرات، تحديثات حالة الجهاز

مثال من العالم الحقيقي: الإشعارات المباشرة

// الخادم: نظام الإشعارات const express = require('express'); const app = express(); // تخزين الاتصالات النشطة const clients = new Map(); // نقطة نهاية SSE app.get('/api/notifications', (req, res) => { const userId = req.query.userId; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // إضافة العميل إلى الاتصالات النشطة clients.set(userId, res); // إرسال تأكيد الاتصال الأولي res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`); // إبقاء الاتصال حيًا كل 30 ثانية const keepAliveId = setInterval(() => { res.write(':ping\n\n'); }, 30000); req.on('close', () => { clearInterval(keepAliveId); clients.delete(userId); console.log(`المستخدم ${userId} منفصل`); }); }); // نقطة نهاية API لإرسال الإشعار app.post('/api/send-notification', express.json(), (req, res) => { const { userId, notification } = req.body; const client = clients.get(userId); if (client) { client.write(`event: notification\n`); client.write(`data: ${JSON.stringify(notification)}\n\n`); res.json({ success: true }); } else { res.status(404).json({ error: 'المستخدم غير متصل' }); } }); app.listen(3000);
// العميل: واجهة الإشعارات const userId = 'user123'; const eventSource = new EventSource(`/api/notifications?userId=${userId}`); eventSource.addEventListener('notification', (event) => { const notification = JSON.parse(event.data); displayNotification(notification); }); function displayNotification(notification) { // إظهار إشعار المتصفح if (Notification.permission === 'granted') { new Notification(notification.title, { body: notification.message, icon: notification.icon }); } // إظهار الإشعار في التطبيق const notifDiv = document.createElement('div'); notifDiv.className = 'notification'; notifDiv.innerHTML = ` <strong>${notification.title}</strong> <p>${notification.message}</p> `; document.getElementById('notifications').appendChild(notifDiv); // إزالة تلقائية بعد 5 ثوانٍ setTimeout(() => notifDiv.remove(), 5000); }

قيود SSE

قيود مهمة:
  • حد اتصال المتصفح: معظم المتصفحات تحد اتصالات SSE إلى 6 لكل نطاق
  • لا بيانات ثنائية: SSE يدعم فقط نص UTF-8 (WebSockets يدعم الثنائي)
  • اتجاه واحد فقط: لا يمكن للعميل إرسال بيانات عبر اتصال SSE
  • لا خلفية محمولة: قد تغلق متصفحات الجوال الاتصالات عندما يكون التطبيق في الخلفية
  • قيود HTTP/1.1: كل اتصال SSE يستخدم اتصال HTTP واحد (أفضل مع HTTP/2)

التمرين: قم ببناء مدونة مباشرة

قم بإنشاء مدونة في الوقت الفعلي مع SSE:
  1. قم ببناء خادم Express مع نقطة نهاية SSE لمنشورات المدونة
  2. قم بإنشاء صفحة عميل تعرض منشورات المدونة عند نشرها
  3. أضف أنواع أحداث مختلفة: 'post' للمنشورات الجديدة، 'update' للتعديلات، 'delete' للإزالات
  4. قم بتنفيذ إعادة الاتصال التلقائية مع مؤشر مرئي
  5. استخدم معرفات الأحداث للتعامل مع إعادة الاتصال بشكل سلس
  6. أضف واجهة إدارة بسيطة لنشر المنشورات
  7. إضافي: اعرض عدد المشاهدات في الوقت الفعلي لكل منشور

الملخص

  • توفر SSE اتصالًا بسيطًا من الخادم إلى العميل في الوقت الفعلي عبر HTTP
  • استخدم واجهة EventSource API من جانب العميل - إنها مدمجة في جميع المتصفحات الحديثة
  • يعيد SSE الاتصال تلقائيًا عند انقطاع الاتصالات
  • تسمح أنواع الأحداث بتنظيم فئات رسائل مختلفة
  • تتيح معرفات الأحداث الاستئناف من آخر رسالة مستلمة
  • SSE مثالي للإشعارات والموجزات ولوحات المراقبة
  • اختر SSE بدلاً من WebSockets عندما تحتاج فقط إلى اتصال من الخادم إلى العميل
  • كن على دراية بحدود اتصال المتصفح (6 لكل نطاق)