בעת עדכון סטטוס ההזמנה, הלקוח נאלץ לטעון מחדש את העמוד או לשאול את ה-API כל 5 שניות. זה מגביר את העומס על השרת ופוגע בחוויית המשתמש. מצב דומה מתרחש בפלטפורמה פיננסית שבה השערים משתנים כל שנייה—WebSocket הוא מוגזם, אבל שאילתות קבועות הורגות את מסד הנתונים. אנו פותרים בעיה זו עם Server-Sent Events (SSE)—טכנולוגיית סטרימינג חד-כיוונית מהשרת ללקוח דרך HTTP. הניסיון שלנו מראה: SSE מפחית את מספר הבקשות פי 10 ומספק אירועים תוך <1 שנייה.
למה SSE ולא WebSocket?
| קריטריון | SSE | WebSocket |
|---|---|---|
| כיוון | חד-כיווני (שרת→לקוח) | דו-כיווני |
| תאימות HTTP | HTTP/1.1 טבעי | דורש שדרוג |
| חיבור מחדש אוטומטי | מובנה | דורש יישום |
| תאימות פרוקסי | עובד ללא בעיות | לעיתים קרובות חסום |
| CORS | GET ללא preflight | Preflight עבור non-GET |
SSE מתאים יותר למשימות שבהן הודעות ועדכונים חיים חשובים. שליחת נתונים מהלקוח לשרת היא נדירה. הגדרת SSE אורכת בממוצע חצי מהזמן בהשוואה ל-WebSocket. לדוגמה, עבור הודעות סטטוס הזמנה SSE הוא אידיאלי, בעוד שעבור צ'אט WebSocket עדיף.
איך SSE מתקנה על פני מספר שרתים?
בפרויקט אחד למסחר אלקטרוני גדול, יישמנו SSE עם הפצה מבוזרת דרך Redis Pub/Sub. הלקוח השיג הפחתה בזמן עדכון סטטוס ההזמנה מ-5 דקות לשנייה אחת. המערכת מתמודדת עם 10,000 חיבורים מקבילים ללא אובדן אירועים. התקנה מושגת דרך ברוקר הודעות: כל השרתים נרשמים לערוץ משותף ומפרסמים אירועים לכל הלקוחות. השתמשנו בתבנית זו ביותר מ-30 פרויקטים בשנים האחרונות.
יישום טכני
פורמט SSE
תגובת השרת היא text/event-stream עם שדות data:, event:, id:, retry::
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"notification","message":"Новый заказ"}
event: order_update
data: {"orderId":"123","status":"shipped"}
id: msg_456
retry: 3000
: comment (игнорируется клиентом) נקודת קצה בשרת (Node.js + Express)
קוד מלא של נקודת הקצה בשרת עם heartbeat ורישום לקוחות
app.get('/api/events', (req, res) => {
const userId = req.user.id;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // для Nginx: отключить буферизацию
});
// Немедленно отправляем первый пакет (обход nginx буферизации)
res.write(':ok\n\n');
// Добавляем клиента в реестр
const clientId = nanoid();
clients.set(clientId, { res, userId });
// Heartbeat каждые 15 секунд
const heartbeat = setInterval(() => {
res.write(': ping\n\n');
}, 15000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(clientId);
});
});
// Отправка события конкретному пользователю
function sendToUser(userId: string, event: string, data: object) {
clients.forEach(({ res, userId: uid }) => {
if (uid === userId) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
});
}
// Broadcast всем
function broadcast(event: string, data: object) {
clients.forEach(({ res }) => {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
} לקוח המשתמש ב-EventSource
חיבור דרך EventSource עם תמיכה באירועים בעלי שם:
const eventSource = new EventSource('/api/events', { withCredentials: true });
// Дефолтные события (event: без имени)
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Message:', data);
};
// Именованные события
eventSource.addEventListener('order_update', (e) => {
const order = JSON.parse(e.data);
updateOrderStatus(order.orderId, order.status);
});
eventSource.addEventListener('notification', (e) => {
showNotification(JSON.parse(e.data).message);
});
// Обработка ошибок
eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Соединение закрыто, автоматически переподключается...');
}
};לפרטים נוספים על EventSource, עיין בתיעוד הרשמי: MDN Web Docs.
התקנה עם Redis Pub/Sub
sub.subscribe('user:events', (message) => {
const { userId, event, data } = JSON.parse(message);
sendToUser(userId, event, data);
});
// Из любого сервиса
await pub.publish('user:events', JSON.stringify({
userId: 'user_123',
event: 'payment_completed',
data: { amount: 5000 }
}));
דוגמה לאירועים בעלי שם
| אירוע | פורמט נתונים |
|---|---|
HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: {"type":"notification","message":"Новый заказ"} event: order_update data: {"orderId":"123","status":"shipped"} id: msg_456 retry: 3000 : comment (игнорируется клиентом) | app.get('/api/events', (req, res) => { const userId = req.user.id; res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // для Nginx: отключить буферизацию }); // Немедленно отправляем первый пакет (обход nginx буферизации) res.write(':ok\n\n'); // Добавляем клиента в реестр const clientId = nanoid(); clients.set(clientId, { res, userId }); // Heartbeat каждые 15 секунд const heartbeat = setInterval(() => { res.write(': ping\n\n'); }, 15000); req.on('close', () => { clearInterval(heartbeat); clients.delete(clientId); }); }); // Отправка события конкретному пользователю function sendToUser(userId: string, event: string, data: object) { clients.forEach(({ res, userId: uid }) => { if (uid === userId) { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); } }); } // Broadcast всем function broadcast(event: string, data: object) { clients.forEach(({ res }) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }); } |
const eventSource = new EventSource('/api/events', { withCredentials: true }); // Дефолтные события (event: без имени) eventSource.onmessage = (e) => { const data = JSON.parse(e.data); console.log('Message:', data); }; // Именованные события eventSource.addEventListener('order_update', (e) => { const order = JSON.parse(e.data); updateOrderStatus(order.orderId, order.status); }); eventSource.addEventListener('notification', (e) => { showNotification(JSON.parse(e.data).message); }); // Обработка ошибок eventSource.onerror = (e) => { if (eventSource.readyState === EventSource.CLOSED) { console.log('Соединение закрыто, автоматически переподключается...'); } }; | sub.subscribe('user:events', (message) => { const { userId, event, data } = JSON.parse(message); sendToUser(userId, event, data); }); // Из любого сервиса await pub.publish('user:events', JSON.stringify({ userId: 'user_123', event: 'payment_completed', data: { amount: 5000 } })); |
notification | {"type":"success","message":"..."} |
מקרה אמיתי: SSE לפלטפורמה פיננסית
עבור סטארטאפ המספק נתוני מטבעות קריפטוגרפיים בזמן אמת, תכננו מערכת מבוססת SSE. הדרישה הייתה לשלוח שערים עבור יותר מ-50 זוגות מטבעות לאלפי משתמשים ללא עיכוב וללא העמסת מסדי נתונים של לקוחות. פתרון: כל משתמש נרשם לזוגות הרצויים דרך נקודת קצה מותאמת אישית, והשרתים מאגדים אירועים דרך Redis Pub/Sub. תוצאה: זמן השהיה באספקה מתחת ל-200 אלפיות השנייה גם עם 10,000 חיבורים. זה אפשר לנו לנטוש את WebSocket, לפשט את התשתית ולחסוך בפיתוח. אנו מבטיחים יציבות של תכנית זו בעומס גבוה.
איך להבטיח חיבור מחדש אמין?
SSE מתחבר מחדש אוטומטית, אבל חשוב להגדיר heartbeat. אם השרת לא שולח נתונים במשך 30 שניות, הדפדפן עלול לסגור את החיבור. לכן, אנו שולחים תגובה ריקה (order_update) כל 15 שניות. זה שומר על החיבור פעיל. בנוסף, השתמש בכותרת {"orderId":"123","status":"shipped"} עבור Nginx כדי למנוע חציצה של נתונים.
מגבלות SSE
- שרת→לקוח בלבד — הלקוח לא יכול לשלוח נתונים דרך SSE (רק בקשות EventSource חדשות או קריאות API נפרדות).
- מגבלת חיבורים ב-HTTP/1.1 — דפדפנים מגבילים ל-6 חיבורים לכל דומיין. SSE צורך אחד. פתרון: HTTP/2 (זרם מרובה יחיד).
-
אין תמיכה ב-IE — השתמש ב-polyfill
payment_completedעבור דפדפנים ישנים.
מה כלול וטעויות נפוצות
- עיצוב סכמת האירועים וסוגי הנתונים.
- יישום נקודת הקצה בשרת עם אימות ו-heartbeat.
- אינטגרציה בצד הלקוח (React hook, Angular service, או EventSource טבעי).
- בדיקות עומס עד 10,000 חיבורים.
- תיעוד הפרוטוקול והקוד.
- הדרכה לצוות הלקוח.
טעויות נפוצות ביישום SSE:
- שכחת חציצה של Nginx — חסר כותרת
{"amount":5000,"currency":"USD"}. - שימוש ב-SSE לתקשורת דו-כיוונית — WebSocket מתאים יותר.
- אי הגדרת heartbeat — החיבור עלול ליפול ולא להתאושש.
לוחות זמנים
נקודת קצה SSE עם אימות, heartbeat, והתקנת Redis: 3–5 ימים. עם אירועים מוגדרים, hook בצד הלקוח (useSSE), ואינטגרציית הודעות: 1–2 שבועות.
בואו נבחן את הפרויקט שלכם — צרו קשר לייעוץ. במהלך השנים, יישמנו פתרונות רבים מבוססי SSE, ומבטיחים יציבות בעומס גבוה. אם אתם צריכים ליישם עדכונים חיים, קבלו ייעוץ היום.







