Strumieniowanie aktualizacji ze zdarzeniami wysyłanymi przez serwer

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

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

Koncepcja SSE może być Ci znana. Aplikacja internetowa „subskrybuje” strumień aktualizacji wygenerowanych przez serwer i za każdym razem, gdy wystąpi nowe zdarzenie, wysyłane jest do klienta powiadomienie. Aby jednak zrozumieć zdarzenia wysyłane przez serwer, musimy poznać ograniczenia poprzednich aplikacji Obejmuje to m.in.:

  • Odpytywanie: aplikacja wielokrotnie odpytuje serwer o dane. Z tej techniki korzysta większość aplikacji AJAX. W przypadku protokołu HTTP pobieranie danych odbywa się w ramach formatu żądania i odpowiedzi. Klient wysyła żądanie i czeka na odpowiedź serwera z danymi. Jeśli żadna nie jest dostępna, zwracana jest pusta odpowiedź. Dodatkowe odpytywanie zwiększa narzut HTTP.

  • Długie wywoływanie (wywołanie GET / COMET): jeśli serwer nie ma dostępnych danych, zatrzymuje żądanie otwarte do momentu udostępnienia nowych danych. Dlatego ta technika jest często nazywana „zawieszonym żądaniem GET”. Gdy informacje staną się dostępne, serwer odpowie, zamknie połączenie i powtórzy proces. W związku z tym serwer stale odpowiada nowymi danymi. Aby to skonfigurować, deweloperzy zwykle korzystają z takich technik, jak dołączanie tagów skryptu do „nieskończonego” elementu iframe.

Wydarzenia wysyłane przez serwer zostały zaprojektowane od podstaw pod kątem wydajności. Podczas komunikacji z SSE serwer może w dowolnym momencie przekazywać dane do aplikacji bez konieczności wysyłania wstępnego żądania. Innymi słowy, aktualizacje mogą być przesyłane strumieniowo z serwera do klienta w miarę ich pojawiania się. Sesje SSE otwierają jeden, jednokierunkowy kanał między serwerem a klientem.

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

Zdarzenia wysyłane przez serwer a WebSocket

Dlaczego lepiej wybrać zdarzenia wysyłane przez serwer niż zdarzenia WebSockets? Dobre pytanie.

WebSockets ma bogaty protokół z dwukierunkową, pełnodupleksową komunikacją. Kanały dwukierunkowe lepiej sprawdzają się w przypadku gier, aplikacji do przesyłania wiadomości i wszelkich zastosowań, w których potrzebne są aktualizacje w czasie zbliżonym do rzeczywistego w obu kierunkach.

Czasami jednak wystarczy jednokierunkowa komunikacja z serwerem. na przykład gdy znajomy zaktualizuje swój status, kody giełdowe, kanały wiadomości lub inny automatyczny mechanizm przekazywania danych. Innymi słowy, aktualizacja bazy danych Web SQL po stronie klienta lub magazynu obiektów IndexedDB. Jeśli chcesz wysłać dane na serwer, możesz skorzystać z funkcji XMLHttpRequest.

Zapytania SSE są wysyłane przez HTTP. Nie trzeba stosować żadnego specjalnego protokołu ani implementacji serwera. WebSockets wymagają połączeń full-duplex i nowych serwerów WebSocket, aby obsługiwać ten protokół.

Poza tym zdarzenia wysyłane przez serwer mają wiele funkcji, których WebSockets nie obsługuje z powodu swojej konstrukcji, m.in. automatyczne ponowne nawiązywanie połączenia, identyfikatory zdarzeń i możliwość wysyłania dowolnych zdarzeń.

Tworzenie źródła zdarzenia 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 słuchać openerror:

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

Gdy aktualizacje są przesyłane z serwera, uruchamia się moduł obsługi onmessage, a nowe dane są dostępne w usłudze e.data. Co ważne, przy zamykaniu połączenia przeglądarka automatycznie łączy się ze źródłem po około 3 sekundach. Implementacja serwera może nawet kontrolować ten limit czasu ponownego nawiązywania połączenia.

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

Format strumienia zdarzeń

Wysyłanie strumienia zdarzeń ze źródła polega na utworzeniu odpowiedzi tekstowej w postaci zwykłego tekstu udostępnianej za pomocą typu Content-Type text/event-stream zgodnego z formatem SSE. W swojej podstawowej formie odpowiedź powinna zawierać wiersz data:, wiadomość oraz 2 znaki „\n” na końcu strumienia:

data: My message\n\n

Dane wielowierszowe

Jeśli wiadomość jest dłuższa, możesz ją podzielić na kilka data:wierszy. 2 lub więcej kolejnych wierszy zaczynających się od data: są traktowane jako pojedynczy element danych, co oznacza, że uruchamiane jest tylko 1 zdarzenie message.

Każdy wiersz powinien kończyć się pojedynczym znakiem „\n” (z wyjątkiem ostatniego, który powinien kończyć się 2 takimi znakami). Wynik przekazany do obsługi funkcji 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>

Powoduje to generowanie „pierwszego wiersza\ndrugiego wiersza” w języku e.data. Następnie można użyć znaku e.data.split('\n').join(''), aby odtworzyć wiadomość bez znaków „\n”.

Wysyłanie danych w formacie JSON

Użycie wielu wierszy ułatwia wysyłanie danych 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ązanie identyfikatora z wydarzeniem

Możesz wysyłać unikalny identyfikator ze zdarzeniem strumienia, dodając wiersz zaczynający się od id::

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

Ustawienie identyfikatora pozwala przeglądarce śledzić ostatnie wywołane zdarzenie, dzięki czemu w przypadku utraty połączenia z serwerem nowe żądanie będzie zawierać specjalny nagłówek HTTP (Last-Event-ID). Dzięki temu przeglądarka może określić, które zdarzenie ma być uruchamiane. Zdarzenie message zawiera właściwość e.lastEventId.

Kontrolowanie czasu oczekiwania na ponowne połączenie

Po zamknięciu każdego połączenia przeglądarka próbuje nawiązać połączenie ze źródłem w ciągu około 3 sekund. Możesz zmienić ten czas oczekiwania, dodając wiersz zaczynający się od retry:, a następnie podając liczbę milisekund, przez którą ma nastąpić ponowne połączenie.

W tym przykładzie spróbujemy połączyć się ponownie po 10 sekundach:

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

Podaj nazwę zdarzenia

Pojedyncze źródło zdarzeń może generować różne typy zdarzeń, podając nazwę zdarzenia. Jeśli występuje wiersz zaczynający się od event:, a za nim podana jest niepowtarzalna nazwa zdarzenia, oznacza to, że zdarzenie jest z nią powiązane. Na kliencie można skonfigurować detektor zdarzeń, który będzie wykrywać to konkretne zdarzenie.

Na przykład ten fragment danych wyjściowych serwera wysyła 3 typy zdarzeń: ogólne zdarzenie „message”, zdarzenie „userlogon” i zdarzenie „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

Gdy detektory zdarzeń są skonfigurowane 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ń

Zwykle przeglądarka automatycznie nawiązuje połączenie ze źródłem zdarzenia po jego zamknięciu, ale można to anulować na kliencie lub serwerze.

Aby anulować strumień z klienta, wywołaj:

source.close();

Aby anulować strumień z serwera, odpowiedz z wartością inną niż text/event-streamContent-Type lub z kodem stanu HTTP innym niż 200 OK (na przykład 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ą tym samym zasadom dotyczącym pochodzenia co inne interfejsy sieciowe, np. fetch. 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ą współdzielenia zasobów między serwerami z różnych domen (CORS).