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

אירועים שנשלחו על ידי השרת (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.

הפורמט של עדכוני האירוע

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

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

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

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

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

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