Strumieniowanie aktualizacji ze zdarzeniami wysyłanymi przez serwer

Eric Bidelman

Zdarzenia wysłane przez serwer Wysyłają automatyczne aktualizacje do klienta z serwera przy użyciu protokołu HTTP połączenia. Po nawiązaniu połączenia serwery mogą inicjować dane transmisji danych.

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

Pojęcie SSE może wydawać się znajome. Aplikacja internetowa „subskrybuje” do strumienia aktualizacje generowane przez serwer oraz przy każdym wystąpieniu nowego zdarzenia wysyłane jest powiadomienie jest wysyłana do klienta. Aby jednak zrozumieć zdarzenia wysyłane przez serwer, i poznać ograniczenia jej poprzedników AJAX. Obejmuje to m.in.:

  • Odpytywanie: aplikacja wielokrotnie odpytuje serwer w poszukiwaniu danych. Ta technika jest używany w większości aplikacji AJAX. Za pomocą protokołu HTTP pobieranie dotyczy głównie formatu żądań i odpowiedzi. Klient wysyła żądanie i czeka na odpowiedź serwera z danymi. Jeśli żadna nie jest dostępna, zostanie puste pole . Dodatkowe odpytywanie zwiększa narzut HTTP.

  • Długie odpytywanie (zawieszanie GET / COMET): jeśli serwer nie ma danych serwer nie zamyka żądania, dopóki nowe dane nie zostaną udostępnione. Dlatego technika ta jest często nazywana „zawieszaniem się GET”. Kiedy informacje, serwer odpowiada, zamyka połączenie, a proces się powtarza. W związku z tym serwer stale odpowiada przy użyciu nowych danych. Aby to skonfigurować, deweloperzy zwykle używają technik hakerskich, takich jak dodawanie Script na wartość „nieskończony” iframe.

Zdarzenia wysyłane przez serwer zostały od podstaw zaprojektowane pod kątem maksymalnej wydajności. Podczas komunikowania się z SSE serwer może przekazywać dane do gdy tylko zechce, bez konieczności wcześniejszego żądania. Innymi słowy, mogą być na bieżąco przesyłane strumieniowo z serwera do klienta. Sesje SSE otworzyć jeden kanał jednokierunkowy między serwerem a klientem.

Główną różnicą między zdarzeniami wysyłanymi przez serwer a długimi ankietami jest to, że SSE są obsługiwane bezpośrednio przez przeglądarkę i użytkownik musi tylko nasłuchiwać wiadomości.

Zdarzenia wysyłane przez serwer a WebSockets

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

WebSockets ma bogaty protokół w komunikacji dwukierunkowej i pełnodupleksowej. Kanał dwukierunkowy jest lepszy gier, komunikatorów i innych zastosowań, które wymagają aktualizacji w czasie zbliżonym do rzeczywistego, w obu kierunkach.

Czasami jednak potrzebna jest tylko jednokierunkowa komunikacja z serwerem. na przykład gdy znajomy zaktualizuje swój status, pasek giełdowy, kanały wiadomości lub innych automatycznych mechanizmów przekazywania danych. Innymi słowy, aktualizację bazy danych Web SQL lub magazynu obiektów IndexedDB po stronie klienta. Jeśli musisz wysyłać dane na serwer, XMLHttpRequest zawsze jest znajomym.

Zapytania SSE są wysyłane przez HTTP. Nie ma specjalnego protokołu ani serwera aby rozpocząć pracę. WebSockets wymaga pełnego dupleksu a także nowe serwery WebSocket do obsługi protokołu.

Poza tym zdarzenia wysyłane przez serwer mają wiele funkcji, których nie zapewnia WebSockets. obejmuje automatyczne ponowne łączenie, identyfikatory zdarzeń dowolne zdarzenia.

Tworzenie elementu EventSource za pomocą JavaScriptu

Aby zasubskrybować strumień zdarzeń, utwórz obiekt EventSource i przekaż go Adres URL transmisji:

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

Następnie skonfiguruj moduł obsługi zdarzenia message. Opcjonalnie możesz nasłuchuj przez 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 i są dostępne nowe dane w jej usłudze w e.data. Magiczna część to że po zamknięciu połączenia przeglądarka automatycznie łączy się ponownie po około 3 sekundach. Implementacja serwera może nawet mieć kontrolę nad ten limit czasu na ponowne połączenie.

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 zwykły tekst wyświetlany z parametrem Content-Type text/event-stream, zgodny z formatem SSE. W podstawowej postaci odpowiedź powinna zawierać wiersz data:, po którym następuje ciąg wiadomość, a po niej dwa „\n” znaki na zakończenie strumienia:

data: My message\n\n

Dane wielowierszowe

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

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

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

Powstanie „pierwsza linia\ndruga linia”. w usłudze e.data. Można następnie użyć e.data.split('\n').join(''), aby zrekonstruować wiadomość „\n” znaków.

Wyślij dane JSON

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

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

I ewentualny 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 ze zdarzeniem

Ze zdarzeniem strumienia możesz wysyłać unikalny identyfikator, dodając wiersz rozpoczynający się od id::

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

Ustawienie identyfikatora pozwala przeglądarce śledzić ostatnie uruchomione zdarzenie, dzięki czemu, jeśli połączenie z serwerem zostanie przerwane, zostanie dodany specjalny nagłówek HTTP (Last-Event-ID), z nowym żądaniem. 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

Przeglądarka próbuje ponownie połączyć się ze źródłem przez około 3 sekundy po zamknięciu każdego połączenia. Możesz zmienić ten czas oczekiwania, dodając wiersz zaczynający się od retry:, po którym następuje liczba milisekund Odczekaj przed ponowną próbą ponownego połączenia.

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

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

Podaj nazwę zdarzenia

Jedno źródło zdarzeń może generować zdarzenia różnych typów poprzez dodanie parametru nazwę zdarzenia. Jeśli występuje wiersz zaczynający się od event:, z unikalną nazwą zdarzenia, zostanie ono powiązane z tą nazwą. Można skonfigurować u klienta detektor zdarzeń, aby nasłuchiwał tego konkretnego zdarzenia.

Na przykład poniższe dane wyjściowe serwera wysyłają 3 typy zdarzeń: ogólny „komunikat” event, „userlogon” i „update” zdarzenie:

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 wykorzystaniem Moduł 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 ponownie łączy się ze źródłem zdarzeń po nawiązaniu połączenia jest zamknięte, ale to działanie można anulować z poziomu klienta lub serwera.

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

source.close();

Aby anulować strumień z serwera, w odpowiedzi użyj elementu innego 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 tego samego źródła co lub inne sieciowe interfejsy API, np. do pobierania. Jeśli punkt końcowy SSE na serwerze ma być dostępne z różnych źródeł, przeczytaj, jak włączyć CoRS.