סטרימינג של עדכונים עם אירועים שנשלחו על ידי השרת

אירועים שנשלחו על ידי שרת (SSE) שולחים עדכונים אוטומטיים ללקוח משרת, עם חיבור HTTP. אחרי יצירת החיבור, השרתים יכולים להתחיל את העברת הנתונים.

כדאי להשתמש ב-SSE כדי לשלוח התראות מאפליקציית האינטרנט. פלטפורמות SSE שולחות מידע בכיוון אחד, ולכן לא יתקבלו עדכונים מהלקוח.

יכול להיות שהקונספט של SSE כבר מוכר לכם. אפליקציית אינטרנט "נרשמים" לזרם העדכונים שנוצר על ידי השרת, ובכל פעם שמתרחש אירוע חדש, נשלחת התראה ללקוח. אבל כדי להבין באמת אירועים שנשלחו באמצעות שרת, אנחנו צריכים להבין את המגבלות של קודמי AJAX שלו. דוגמאות לתכנים כאלה:

  • סקרים: האפליקציה בודקת את השרת שוב ושוב כדי לאתר נתונים. השיטה הזו משמשת ברוב האפליקציות של AJAX. באמצעות פרוטוקול HTTP, אחזור הנתונים מתנהל סביב בקשה ופורמט תגובה. הלקוח שולח בקשה וממתין שהשרת יגיב עם הנתונים. אם אין תשובה זמינה, תוחזר תגובה ריקה. תשאול נוספת יוצרת תקורת HTTP גדולה יותר.

  • סקרים ארוכים (Hanging GET / COMET): אם לשרת אין נתונים זמינים, הבקשה תישאר פתוחה עד שיהיו נתונים חדשים זמינים. לכן, הטכניקה הזו נקראת בדרך כלל "Hanging GET". כשהמידע הופך לזמין, השרת מגיב, סוגר את החיבור והתהליך חוזר על עצמו. לכן השרת מגיב כל הזמן עם נתונים חדשים. כדי להגדיר זאת, מפתחים משתמשים בדרך כלל בפריצות כמו צירוף תגי סקריפט ל-iframe 'אינסופי'.

אירועים שנשלחים על ידי השרת תוכננו מהיסוד כך שיהיו יעילים. השרת שמתקשרים עם SSE יכול לדחוף נתונים לאפליקציה מתי שרוצים, בלי לשלוח בקשה ראשונית. במילים אחרות, ניתן לשדר עדכונים משרת ללקוח בזמן שהם מתרחשים. מהנדסי SSE פותחים ערוץ חד-כיווני אחד בין השרת ללקוח.

ההבדל העיקרי בין אירועים שנשלחים על ידי שרת לבין סקרים ארוכים הוא שה-SSE מטופל ישירות על ידי הדפדפן, והמשתמש רק צריך להאזין להודעות.

אירועים שנשלחו על ידי שרת לעומת WebSockets

למה עדיף לבחור אירועים שנשלחו על ידי שרת על פני WebSockets? שאלה טובה.

ל-WebSockets יש פרוטוקול עשיר עם תקשורת דו-כיוונית ודו-צדדית מלאה. ערוץ דו-כיווני מתאים יותר למשחקים, לאפליקציות להעברת הודעות ולכל תרחיש לדוגמה שבו צריך עדכונים כמעט בזמן אמת בשני הכיוונים.

עם זאת, לפעמים צריך רק תקשורת חד-כיוונית מהשרת. לדוגמה, כשחבר מעדכן את הסטטוס שלו, מידע על מניות, עדכוני חדשות או מנגנונים אוטומטיים אחרים לדחיפה של נתונים. במילים אחרות, עדכון למסד נתוני Web SQL בצד הלקוח או לאחסון אובייקטים של IndexedDB. כששולחים נתונים לשרת, XMLHttpRequest הוא תמיד חבר.

SSE נשלחים באמצעות HTTP. אין הטמעה מיוחדת של פרוטוקול או שרת כדי להתחיל לעבוד. WebSockets מחייבים חיבורים מלאים דו-כיווניים ושרתי WebSocket חדשים כדי לטפל בפרוטוקול.

בנוסף, לאירועים שנשלחים על ידי השרת יש מגוון תכונות שחסרות ב-WebSockets, כולל חיבור מחדש אוטומטי, מזהי אירועים והיכולת לשלוח אירועים שרירותיים.

יצירת EventSource באמצעות JavaScript

כדי להירשם לזרם אירועים, צריך ליצור אובייקט EventSource ולהעביר אותו לכתובת ה-URL של השידור:

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

בשלב הבא צריך להגדיר handler לאירוע 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.
  }
});

כשעדכונים נשלחים מהשרת, ה-handler של onmessage מופעל ונתונים חדשים זמינים במאפיין e.data שלו. החלק הקסום הוא שבכל פעם שהחיבור נסגר, הדפדפן מתחבר מחדש למקור באופן אוטומטי אחרי כ-3 שניות. להטמעת השרת יכולה להיות אפילו שליטה על הזמן הקצוב לתפוגה של התחברות מחדש.

זה הכול. הלקוח שלך יכול עכשיו לעבד אירועים מתוך stream.php.

הפורמט של מקור האירוע

שליחת סטרימינג של אירוע מהמקור היא יצירה של תגובה בטקסט פשוט, המוצגת עם סוג תוכן text/event-stream, בהתאם לפורמט SSE. בצורתו הבסיסית, התשובה צריכה לכלול את השורה data:, אחריה את ההודעה שלך, ואז שני תווי '\n' לסיום השידור:

data: My message\n\n

נתונים מרובי שורות

אם ההודעה ארוכה יותר, אפשר לפצל אותה באמצעות כמה שורות data:. שתי שורות או יותר המתחילות ב-data: נחשבות כקטע נתונים יחיד, כלומר רק אירוע message אחד מופעל.

כל שורה צריכה להסתיים בתו "\n" אחד (מלבד השורה האחרונה, שאמורה להסתיים בשניים). התוצאה שמועברת ל-handler של message היא מחרוזת יחידה שמשורשרת על ידי תווים בשורה החדשה. למשל:

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

הפעולה הזו יוצרת 'שורה ראשונה\nהשורה השנייה' בטווח e.data. לאחר מכן אפשר להשתמש ב-e.data.split('\n').join('') כדי לשחזר את ההודעה עם תווי \n "\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: ואחריה שם ייחודי לאירוע, האירוע משויך לשם הזה. בלקוח, ניתן להגדיר event listener כך שיאזין לאירוע הספציפי הזה.

לדוגמה, פלט השרת הבא שולח שלושה סוגי אירועים: אירוע '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

כשמגדירים פונקציות event listener אצל הלקוח:

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 באמצעות handler של 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();

כדי לבטל שידור מהשרת, צריך להשיב עם המזהה Content-Type שאינו text/event-stream או להחזיר סטטוס HTTP שהוא לא 200 OK (כמו 404 Not Found).

שתי הדרכים מונעות מהדפדפן ליצור מחדש את החיבור.

כמה מילים על אבטחה

בקשות שנוצרות על ידי EventSource כפופות לאותם כללי מדיניות מקור כמו ממשקי API אחרים של רשת, כמו אחזור. אם אתם צריכים שנקודת הקצה של SSE בשרת תהיה נגישה ממקורות שונים, כדאי לקרוא איך מפעילים אותה באמצעות שיתוף משאבים בין מקורות (CORS).