المستوى: متوسط
لو عايز تبعت notifications للمستخدم لحظة وصول رسالة جديدة بدون ما المتصفح يسأل السيرفر كل ثانية، Server-Sent Events (SSE) بيعمل ده في 60 سطر JavaScript بدون مكتبات وبدون التعقيد بتاع WebSocket. الكود اللي قدامك شغّال على Node.js و Express، وفي آخر المقال هتلاقي أرقام مقاسة على 5,000 client متزامن.
Real-time Notifications بدون تعقيد WebSocket
المشكلة باختصار
الطريقة التقليدية بتخلّي المتصفح يسأل السيرفر كل 5 ثواني "في حاجة جديدة؟" — ده اسمه polling. لو عندك 10 آلاف مستخدم online، ده 2,000 طلب في الثانية بيوصلوا للسيرفر، 95% منهم بيرجعوا فاضيين. الـ DB بتاكل CPU، الـ load balancer بيلهث، والمستخدم لسه بيستنى 5 ثواني علشان يشوف رسالة جت لسه.
الـ WebSocket بيحل المشكلة، بس بتكلفة. لازم تتعامل مع protocol upgrade، connection state، reconnection logic يدوي، sticky sessions على الـ load balancer، وتعديلات على الـ proxy. لمعظم الحالات اللي إنت بتبعت فيها بس (server → client بدون ما المستخدم يبعت حاجة)، الحل أبسط بكتير.
المفهوم: SSE بمثال راديو الأخبار
تخيّل راديو محطة أخبار. أنت بتفتحه، بتسمع الأخبار طول ما هو شغّال، ومش لازم تتصل بمحطة الراديو علشان تعرف الجديد. الراديو طول ما متصل، بيستقبل البث بشكل مستمر، والمحطة هي اللي بتقرر إمتى تبعت خبر جديد.
Server-Sent Events بيشتغل بنفس المنطق بالظبط. المتصفح بيفتح اتصال HTTP عادي مع السيرفر، بس بدل ما السيرفر يقفل الاتصال بعد ما يبعت الرد، بيخلّيه مفتوح ويبعت أحداث (events) كل ما حصل تحديث. المتصفح بيستقبلهم تلقائياً عبر EventSource API الـ built-in.
التعريف العلمي: SSE هو بروتوكول standardized في HTML Living Standard من WHATWG. بيستخدم HTTP/1.1 أو HTTP/2 مع Content-Type: text/event-stream، وبيعتمد على keep-alive connection علشان السيرفر يقدر يبعت الـ events على نفس الاتصال. الفرق الجوهري عن WebSocket إن SSE one-directional (من السيرفر للمتصفح فقط)، وبيشتغل فوق HTTP عادي بدون protocol upgrade لـ ws://.
الكود الكامل: السيرفر بـ Node.js
import express from 'express';
const app = express();
const clients = new Set();
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
});
res.write(': connected\n\n');
clients.add(res);
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(res);
});
});
function broadcast(event, data) {
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
for (const client of clients) client.write(payload);
}
app.post('/notify', express.json(), (req, res) => {
broadcast('notification', req.body);
res.json({ sent: clients.size });
});
app.listen(3000);