Strumieniowanie aktualizacji ze zdarzeniami wysyłanymi przez serwer

Zdarzenia wysyłane przez serwer (SSE) wysyłają automatyczne aktualizacje do klienta z serwera przez połączenie HTTP. Po nawiązaniu połączenia serwery mogą zainicjować przesyłanie danych.

Do wysyłania powiadomień push z aplikacji internetowej możesz używać SSE. SSE wysyłają informacje w jednym kierunku, więc nie będziesz otrzymywać aktualizacji od klienta.

Pojęcie SSE może być już znane. Aplikacja internetowa „subskrybuje” strumień aktualizacji generowanych przez serwer i za każdym razem, gdy wystąpi nowe zdarzenie, do klienta wysyłane jest powiadomienie. Aby jednak naprawdę zrozumieć zdarzenia wysyłane przez serwer, musimy poznać ograniczenia jej poprzedników w technologii AJAX. Obejmuje to m.in.:

  • Odpytywanie: aplikacja wielokrotnie wysyła zapytania do serwera w celu uzyskania danych. Ta technika jest używana w większości aplikacji AJAX. W ramach protokołu HTTP pobieranie danych opiera się na formacie żądania i odpowiedzi. Klient wysyła żądanie i oczekuje na odpowiedź serwera z danymi. Jeśli żadna nie jest dostępna, zwracana jest pusta odpowiedź. Dodatkowe odpytywanie zwiększa obciążenie HTTP.

  • Długie odpytywanie (Hanging GET / COMET): jeśli serwer nie ma dostępnych danych, serwer pozostawia żądanie otwarte do czasu udostępnienia nowych danych. Z tego powodu ta technika jest często nazywana „wysunięciem GET”. Gdy informacje stają się dostępne, serwer odpowiada, zamyka połączenie i ten proces jest powtarzany. W związku z tym serwer stale odpowiada za pomocą nowych danych. Aby to skonfigurować, programiści zwykle stosują takie sposoby, jak dołączanie tagów skryptu do „nieskończonego” elementu iframe.

Zdarzenia wysyłane przez serwer zostały od podstaw zaprojektowane tak, aby były wydajne. Komunikując się z SSE, serwer może przekazać dane do Twojej aplikacji w dowolnym momencie, bez konieczności wysyłania wstępnego żądania. Innymi słowy, aktualizacje mogą być przesyłane strumieniowo z serwera do klienta na bieżąco. SSE otwierają 1 jednokierunkowy kanał między serwerem a klientem.

Główna różnica między zdarzeniami wysyłanymi przez serwer a długim czasem badania polega na tym, że SSE są obsługiwane bezpośrednio przez przeglądarkę, a użytkownik musi nasłuchiwać wiadomości.

Zdarzenia wysyłane przez serwer a WebSockets

Dlaczego lepiej wybrać zdarzenia wysyłane przez serwer, a nie WebSockets? Dobre pytanie.

WebSockets ma obszerny protokół z dwukierunkową komunikacją o pełnym dupleksie. Kanał dwukierunkowy sprawdza się lepiej w przypadku gier, komunikatorów i innych przypadków, w których potrzebujesz aktualizacji w czasie zbliżonym do rzeczywistego w obu kierunkach.

Czasami jednak potrzebujesz tylko jednokierunkowej komunikacji z serwera. Dotyczy to na przykład sytuacji, gdy znajomy aktualizuje swój stan, notowania giełdowe, kanały wiadomości lub inne automatyczne mechanizmy przekazywania danych. Innymi słowy, aktualizacja bazy danych Web SQL po stronie klienta lub magazynu obiektów IndexedDB. Jeśli musisz wysłać dane na serwer, XMLHttpRequest zawsze należy do znajomych.

Zapytania SSE są wysyłane przez HTTP. Nie ma specjalnego protokołu ani wdrożenia serwera. WebSocket wymagają połączeń w pełni dupleksu i nowych serwerów WebSocket do obsługi protokołu.

Poza tym zdarzenia wysyłane przez serwer mają wiele funkcji, których WebSockets z założenia nie ma, takie jak automatyczne ponowne połączenie, identyfikatory zdarzeń i możliwość wysyłania dowolnych zdarzeń.

Tworzenie obiektu EventSource za pomocą JavaScriptu

Aby zasubskrybować strumień zdarzeń, utwórz obiekt EventSource i przekaż mu adres URL strumienia:

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

Następnie skonfiguruj moduł obsługi zdarzenia message. Opcjonalnie możesz odsłuchać utwory open i 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.
  }
});

Po przekazaniu aktualizacji z serwera uruchamia się moduł obsługi onmessage, dzięki czemu nowe dane są dostępne we właściwości e.data. Magiczne jest to, że po zamknięciu połączenia przeglądarka automatycznie połączy się ze źródłem po około 3 sekundach. Implementacja serwera może nawet mieć kontrolę nad tym limitem czasu ponownego połączenia.

To wszystko. Twój klient może teraz przetwarzać wydarzenia z domeny stream.php.

Format strumienia zdarzeń

Wysyłanie strumienia zdarzeń ze źródła polega na utworzeniu odpowiedzi w postaci zwykłego tekstu obsługiwanej za pomocą typu treści text/event-stream zgodnego z formatem SSE. W podstawowej postaci odpowiedź powinna zawierać wiersz data:, po którym następuje wiadomość i 2 znaki „\n”, aby zakończyć transmisję:

data: My message\n\n

Dane wielowierszowe

Jeśli Twoja wiadomość jest dłuższa, możesz ją podzielić, używając kilku wierszy data:. Co najmniej 2 kolejne wiersze zaczynające się od data: są traktowane jako jeden fragment danych, co oznacza, że wywoływane jest tylko jedno zdarzenie message.

Każdy wiersz powinien kończyć się pojedynczym znakiem „\n” (z wyjątkiem ostatniego, który powinien kończyć się dwoma). Wynik przekazywany do modułu obsługi message to pojedynczy ciąg znaków połączony znakami nowego wiersza. Na przykład:

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

Spowoduje to utworzenie „pierwszego wiersza\ndrugiego wiersza” w funkcji e.data. Można następnie użyć e.data.split('\n').join('') do zrekonstruowania wiadomości bez znaków „\n”.

Wyślij dane JSON

Użycie wielu wierszy ułatwia wysyłanie JSON bez naruszania składni:

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

I możliwy kod po stronie klienta do obsługi tego strumienia:

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

Powiązywanie identyfikatora ze zdarzeniem

Ze zdarzeniem strumienia możesz wysłać unikalny identyfikator, podając wiersz zaczynający się od id::

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

Po ustawieniu identyfikatora przeglądarka może śledzić ostatnie uruchomione zdarzenie, dzięki czemu w przypadku utraty połączenia z serwerem w odpowiedzi na nowe żądanie pojawia się specjalny nagłówek HTTP (Last-Event-ID). Dzięki temu przeglądarka może określić, które zdarzenie może zostać uruchomione. Zdarzenie message zawiera właściwość e.lastEventId.

Kontroluj czas oczekiwania na połączenie

Przeglądarka próbuje ponownie połączyć się ze źródłem po około 3 sekundach od zakończenia każdego połączenia. Możesz zmienić ten limit czasu, dodając wiersz zaczynający się od retry:, po którym następuje próba ponownego nawiązania połączenia w milisekundach.

Ten przykład pokazuje próbę ponownego nawiązania połączenia po 10 sekundach:

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

Podaj nazwę zdarzenia

Pojedyncze źródło zdarzeń może generować zdarzenia różnego typu dzięki dodaniu nazwy zdarzenia. Jeśli występuje wiersz zaczynający się od event:, po którym znajduje się unikalna nazwa zdarzenia, zdarzenie jest z nią powiązane. Na kliencie można skonfigurować detektor zdarzeń tak, by nasłuchiwał tego konkretnego zdarzenia.

Na przykład te dane wyjściowe z serwera wysyłają 3 typy zdarzeń: ogólne zdarzenie „message”, „userlogon” i „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

Po skonfigurowaniu detektorów zdarzeń na kliencie:

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

Przykłady serwerów

Oto podstawowa implementacja serwera w języku 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()));
?>

Oto podobna implementacja w Node JS z użyciem modułu obsługi 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>

Anulowanie strumienia zdarzeń

Normalnie po zamknięciu połączenia przeglądarka automatycznie ponownie łączy się ze źródłem zdarzeń, ale to działanie może zostać anulowane przez klienta lub serwer.

Aby anulować przesyłanie strumieniowe z klienta, wywołaj:

source.close();

Aby anulować strumień z serwera, wyślij odpowiedź inną niż text/event-stream Content-Type lub zwróć stan HTTP inny niż 200 OK (np. 404 Not Found).

Obie metody uniemożliwiają przeglądarce ponowne nawiązanie połączenia.

Kilka słów o bezpieczeństwie

Żądania generowane przez EventSource podlegają zasadom dotyczącym tego samego źródła co inne interfejsy API sieci, takie jak pobieranie. Jeśli chcesz, aby punkt końcowy SSE na serwerze był dostępny z różnych źródeł, dowiedz się, jak go włączyć za pomocą CORS.