Updates mit vom Server gesendeten Ereignissen streamen

Server-sent Events (SSEs) senden automatische Updates von einem Server über eine HTTP-Verbindung an einen Client. Sobald die Verbindung hergestellt ist, können Server die Datenübertragung initiieren.

Mithilfe von serverseitigen Bedrohungserkennung (SSEs) können Sie Push-Benachrichtigungen von Ihrer Webanwendung senden. SEs senden Informationen in eine Richtung, sodass Sie keine Updates vom Client erhalten.

Das Konzept der serverseitigen Anzeigen ist Ihnen vielleicht bekannt. Eine Webanwendung „abonniert“ einen von einem Server generierten Aktualisierungsstream. Wenn ein neues Ereignis eintritt, wird eine Benachrichtigung an den Client gesendet. Um jedoch vom Server gesendete Ereignisse wirklich zu verstehen, müssen wir die Einschränkungen der AJAX-Vorgänger verstehen. Dazu zählen:

  • Polling: Die Anwendung fragt wiederholt Daten von einem Server ab. Diese Technik wird von den meisten AJAX-Anwendungen verwendet. Beim HTTP-Protokoll dreht sich das Abrufen von Daten um ein Anfrage- und Antwortformat. Der Client stellt eine Anfrage und wartet darauf, dass der Server mit Daten antwortet. Wenn keine verfügbar sind, wird eine leere Antwort zurückgegeben. Zusätzliches Polling verursacht einen höheren HTTP-Overhead.

  • Long Polling (Hanging GET / COMET): Wenn auf dem Server keine Daten verfügbar sind, hält der Server die Anfrage so lange offen, bis neue Daten verfügbar sind. Daher wird diese Technik oft als „Hanging GET“ bezeichnet. Wenn Informationen verfügbar sind, antwortet der Server, trennt die Verbindung und der Vorgang wird wiederholt. Der Server antwortet also ständig mit neuen Daten. Entwickler nutzen dazu in der Regel Hacks, wie z. B. das Anhängen von Skript-Tags an einen „unendlichen“ iFrame.

Vom Server gesendete Ereignisse wurden von Grund auf im Hinblick auf Effizienz konzipiert. Bei der Kommunikation mit SSEs kann ein Server jederzeit Daten an Ihre Anwendung senden, ohne dass eine erste Anfrage gestellt werden muss. Mit anderen Worten: Aktualisierungen können von Server zu Client gestreamt werden, sobald sie stattfinden. SSEs öffnen einen einzigen unidirektionalen Kanal zwischen Server und Client.

Der Hauptunterschied zwischen vom Server gesendeten Ereignissen und langen Abfragen besteht darin, dass SSEs direkt vom Browser verarbeitet werden und der Nutzer nur auf Nachrichten warten muss.

Vom Server gesendete Ereignisse und WebSockets im Vergleich

Warum würden Sie vom Server gesendete Ereignisse gegenüber WebSockets verwenden? Gute Frage.

WebSockets hat ein umfangreiches Protokoll mit bidirektionaler Vollduplex-Kommunikation. Ein bidirektionaler Kanal ist besser für Spiele, Messaging-Apps und alle Anwendungsfälle geeignet, bei denen Aktualisierungen nahezu in Echtzeit in beide Richtungen erforderlich sind.

Manchmal benötigen Sie jedoch nur eine Einwegkommunikation von einem Server. Zum Beispiel, wenn ein Freund seinen Status, Börsenticker, Nachrichtenfeeds oder andere automatisierte Daten-Push-Mechanismen aktualisiert. Mit anderen Worten, eine Aktualisierung einer clientseitigen Web SQL-Datenbank oder eines IndexedDB-Objektspeichers. Wenn du Daten an einen Server senden musst, ist XMLHttpRequest immer ein Freund.

SSEs werden über HTTP gesendet. Es gibt keine spezielle Protokoll- oder Serverimplementierung. WebSockets erfordern Vollduplex-Verbindungen und neue WebSocket-Server zur Verarbeitung des Protokolls.

Darüber hinaus verfügen vom Server gesendete Ereignisse über eine Reihe von Funktionen, die WebSockets fehlen, einschließlich der automatischen Neuverbindung, Ereignis-IDs und der Möglichkeit, beliebige Ereignisse zu senden.

EventSource mit JavaScript erstellen

Wenn Sie einen Ereignisstream abonnieren möchten, erstellen Sie ein EventSource-Objekt und übergeben Sie ihm die URL Ihres Streams:

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

Als Nächstes richten Sie einen Handler für das message-Ereignis ein. Optional können Sie auf open und error warten:

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.
  }
});

Wenn Aktualisierungen vom Server übertragen werden, wird der onmessage-Handler ausgelöst und neue Daten sind im Attribut e.data verfügbar. Das Besondere ist, dass der Browser bei jeder Verbindung nach etwa 3 Sekunden automatisch wieder eine Verbindung zur Quelle herstellt. Ihre Serverimplementierung kann sogar das Zeitlimit für die erneute Verbindung steuern.

Das wars. Ihr Kunde kann jetzt Termine aus stream.php verarbeiten.

Format des Ereignisstreams

Zum Senden eines Ereignisstreams aus der Quelle wird eine Nur-Text-Antwort erstellt, die mit einem text/event-stream Content-Type im SSE-Format bereitgestellt wird. In ihrer einfachen Form sollte die Antwort die Zeile data: enthalten, auf die Ihre Nachricht folgt, gefolgt von zwei „\n“-Zeichen, um den Stream zu beenden:

data: My message\n\n

Mehrzeilige Daten

Wenn Ihre Nachricht länger ist, können Sie sie in mehrere data:-Zeilen aufteilen. Zwei oder mehr aufeinanderfolgende Zeilen, die mit data: beginnen, werden als einzelnes Datenelement behandelt, d. h., nur ein message-Ereignis wird ausgelöst.

Jede Zeile sollte mit einem einzelnen "\n" enden. Eine Ausnahme bildet die letzte Zeile, die mit zwei enden sollte. Das Ergebnis, das an den message-Handler übergeben wird, ist ein einzelner String, der durch Zeilenumbruchzeichen verkettet ist. Beispiel:

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

Dadurch wird „erste Zeile\nzweite Zeile“ in e.data erzeugt. Man könnte dann e.data.split('\n').join('') verwenden, um die Nachricht ohne die „\n“-Zeichen zu rekonstruieren.

JSON-Daten senden

Durch die Verwendung mehrerer Zeilen können Sie JSON-Dateien senden, ohne die Syntax zu beeinträchtigen:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

Und möglicher clientseitiger Code zur Verarbeitung dieses Streams:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

ID mit einem Ereignis verknüpfen

Sie können eine eindeutige ID mit einem Streamereignis senden, indem Sie eine Zeile einfügen, die mit id: beginnt:

id: 12345\n
data: GOOG\n
data: 556\n\n

Durch das Festlegen einer ID kann der Browser das letzte ausgelöste Ereignis verfolgen. Wenn die Verbindung zum Server unterbrochen wird, wird mit der neuen Anfrage ein spezieller HTTP-Header (Last-Event-ID) festgelegt. So kann der Browser bestimmen, welches Ereignis ausgelöst werden soll. Das message-Ereignis enthält eine e.lastEventId-Eigenschaft.

Zeitüberschreitung beim erneuten Herstellen der Verbindung festlegen

Ungefähr drei Sekunden nach dem Beenden einer Verbindung versucht der Browser, die Verbindung zur Quelle wiederherzustellen. Sie können dieses Zeitlimit ändern, indem Sie eine Zeile einfügen, die mit retry: beginnt, gefolgt von der Anzahl der Millisekunden, die bis zum Verbindungsaufbau gewartet werden soll.

Im folgenden Beispiel wird nach 10 Sekunden versucht, die Verbindung wiederherzustellen:

retry: 10000\n
data: hello world\n\n

Geben Sie einen Ereignisnamen an

Eine einzelne Ereignisquelle kann mit einem Ereignisnamen verschiedene Ereignistypen generieren. Wenn eine Zeile vorhanden ist, die mit event: beginnt, gefolgt von einem eindeutigen Namen für das Ereignis, wird das Ereignis mit diesem Namen verknüpft. Auf dem Client kann ein Event-Listener eingerichtet werden, der auf dieses bestimmte Ereignis wartet.

Die folgende Serverausgabe sendet beispielsweise drei Arten von Ereignissen, ein generisches Ereignis „message“, „userlogon“ und ein „update“-Ereignis:

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

Mit Event-Listenern, die auf dem Client eingerichtet sind:

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}`);
};

Serverbeispiele

Hier ist eine grundlegende Serverimplementierung in 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()));
?>

Hier ist eine ähnliche Implementierung für Node JS mit einem Express-Handler:

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>

Ereignisstream abbrechen

Normalerweise stellt der Browser automatisch wieder eine Verbindung zur Ereignisquelle her, wenn die Verbindung getrennt wurde. Dieses Verhalten kann jedoch entweder vom Client oder vom Server unterbunden werden.

Um einen Stream über den Client abzubrechen, rufen Sie folgenden Befehl auf:

source.close();

Wenn Sie einen Stream vom Server abbrechen möchten, antworten Sie mit einem anderen Wert als text/event-stream Content-Type oder geben Sie einen anderen HTTP-Status als 200 OK zurück (z. B. 404 Not Found).

Beide Methoden verhindern, dass der Browser die Verbindung wiederherstellen kann.

Ein Hinweis zum Thema Sicherheit

Von EventSource generierte Anfragen unterliegen denselben Ursprungsrichtlinien wie andere Netzwerk-APIs wie Fetch. Wenn der SSE-Endpunkt auf Ihrem Server von verschiedenen Ursprüngen aus zugänglich sein soll, können Sie Cross-Origin Resource Sharing (CORS) aktivieren.