Aggiornamenti dello streaming con eventi inviati dal server

Gli eventi inviati dal server (SSE) inviano aggiornamenti automatici a un client da un server tramite una connessione HTTP. Una volta stabilita la connessione, i server possono avviare la trasmissione dei dati.

Ti consigliamo di utilizzare le SSE per inviare notifiche push dall'app web. Le SSE inviano le informazioni in una sola direzione, perciò non riceverai aggiornamenti dal client.

Il concetto di SSE potrebbe essere familiare. Un'app web si "abbona" a uno stream di aggiornamenti generati da un server e, ogni volta che si verifica un nuovo evento, viene inviata una notifica al client. Tuttavia, per comprendere davvero gli eventi inviati dal server, dobbiamo comprendere i limiti dei suoi predecessori AJAX. È incluso quanto segue:

  • Polling: l'applicazione esegue ripetutamente il polling di un server per recuperare i dati. Questa tecnica è utilizzata dalla maggior parte delle applicazioni AJAX. Con il protocollo HTTP, il recupero di dati ruota attorno a un formato di richiesta e risposta. Il client effettua una richiesta e attende che il server risponda con i dati. Se non è disponibile, viene restituita una risposta vuota. I controlli periodici aggiuntivi creano un maggiore overhead HTTP.

  • Polling lungo (GET in attesa/COMET): se il server non ha dati disponibili, mantiene la richiesta aperta finché non vengono resi disponibili nuovi dati. Pertanto, questa tecnica è spesso indicata come "GET sospeso". Quando le informazioni diventano disponibili, il server risponde, chiude la connessione e il processo viene ripetuto. Di conseguenza, il server risponde costantemente con nuovi dati. Per configurare questa funzionalità, gli sviluppatori in genere utilizzano hack come l'aggiunta di tag script a un iframe "infinito".

Gli eventi inviati dal server sono stati progettati da zero per essere efficienti. Quando comunica con gli SSE, un server può inviare dati alla tua app in qualsiasi momento, senza dover effettuare una richiesta iniziale. In altre parole, gli aggiornamenti possono essere trasmessi in streaming dal server al client man mano che si verificano. Le SSE aprono un singolo canale unidirezionale tra il server e il client.

La differenza principale tra gli eventi inviati dal server e il polling lungo è che gli SSE vengono gestiti direttamente dal browser e l'utente deve solo ascoltare i messaggi.

Confronto tra eventi inviati dal server e WebSocket

Perché scegliere gli eventi inviati dal server anziché WebSocket? Ottima domanda.

WebSockets ha un protocollo avanzato con comunicazione bidirezionale e full-duplex. Un canale bidirezionale è più adatto per giochi, app di messaggistica e qualsiasi caso d'uso in cui hai bisogno di aggiornamenti quasi in tempo reale in entrambe le direzioni.

Tuttavia, a volte è necessaria solo una comunicazione unidirezionale da un server. Ad esempio, quando un amico aggiorna il proprio stato, le quotazioni di borsa, i feed di notizie o altri meccanismi di push dei dati automatici. In altre parole, un aggiornamento a un database SQL web lato client o a un object store IndexedDB. Se devi inviare dati a un server, XMLHttpRequest è sempre un'opzione valida.

Le SSE vengono inviate tramite HTTP. Non c'è alcuna implementazione speciale del server o del protocollo per iniziare. I WebSocket richiedono connessioni full-duplex e nuovi server WebSocket per gestire il protocollo.

Inoltre, gli eventi inviati dal server dispongono di una serie di funzionalità che mancano ai WebSocket per design, tra cui il ricoinvolgimento automatico, gli ID evento e la possibilità di inviare eventi arbitrari.

Creare un oggetto EventSource con JavaScript

Per iscriverti a uno stream di eventi, crea un oggetto EventSource e passagli l'URL dello stream:

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

Poi, configura un gestore per l'evento message. Se vuoi, puoi monitorare open e 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.
  }
});

Quando gli aggiornamenti vengono inviati dal server, viene attivato l'handler onmessage e i nuovi dati sono disponibili nella relativa proprietà e.data. La cosa magica è che ogni volta che la connessione viene chiusa, il browser si riconnette automaticamente alla fonte dopo circa 3 secondi. L'implementazione del server può persino controllare questo timeout di ricollegamento.

È tutto. Ora il tuo cliente può elaborare gli eventi di stream.php.

Formato del flusso di eventi

L'invio di un flusso di eventi dall'origine consiste nel creare una risposta in testo normale, fornita con un Content-Type text/event-stream, che segue il formato SSE. Nella sua forma di base, la risposta deve contenere una riga data:, seguita dal tuo messaggio, seguito da due caratteri "\n" per terminare lo stream:

data: My message\n\n

Dati su più righe

Se il tuo messaggio è più lungo, puoi suddividerlo utilizzando più righe data:. Due o più righe consecutive che iniziano con data: vengono trattate come un singolo dato, il che significa che viene attivato un solo evento message.

Ogni riga deve terminare con un singolo carattere "\n" (tranne l'ultima, che deve terminare con due). Il risultato passato all'handler message è una singola stringa concatenata da caratteri di nuova riga. Ad esempio:

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

Il risultato è "prima riga\nseconda riga" in e.data. È quindi possibile utilizzare e.data.split('\n').join('') per ricostruire il messaggio senza i caratteri "\n".

Invia dati JSON

L'utilizzo di più righe ti consente di inviare JSON senza interrompere la sintassi:

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

E eventuale codice lato client per gestire lo stream:

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

Associare un ID a un evento

Puoi inviare un ID univoco con un evento stream includendo una riga che inizia con id::

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

L'impostazione di un ID consente al browser di tenere traccia dell'ultimo evento attivato in modo che, se la connessione al server viene interrotta, venga impostato un'intestazione HTTP speciale (Last-Event-ID) con la nuova richiesta. In questo modo il browser può determinare quale evento è appropriato da attivare. L'evento message contiene una proprietà e.lastEventId.

Controllare il valore di riattivazione-timeout

Il browser tenta di riconnettersi all'origine circa 3 secondi dopo la chiusura di ogni connessione. Puoi modificare il timeout includendo una riga che inizia con retry:, seguita dal numero di millisecondi da attendere prima di tentare di riconnettersi.

Nell'esempio seguente viene tentato un nuovo collegamento dopo 10 secondi:

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

Specifica un nome per l'evento

Una singola origine evento può generare tipi di eventi diversi includendo un nome dell'evento. Se è presente una riga che inizia con event:, followed by a unique name for the event, l'evento viene associato a quel nome. Sul client, è possibile configurare un gestore di eventi per ascoltare quel determinato evento.

Ad esempio, l'output del server seguente invia tre tipi di eventi: un evento generico "message", "userlogon" ed "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

Con i listener di eventi configurati sul client:

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

Esempi di server

Ecco un'implementazione di base del server 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()));
?>

Ecco un'implementazione simile su Node JS che utilizza un gestore 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>

Annullare uno stream di eventi

In genere, il browser si ricollega automaticamente all'origine evento quando la connessione viene chiusa, ma questo comportamento può essere annullato dal client o dal server.

Per annullare uno stream dal client, chiama:

source.close();

Per annullare uno stream dal server, rispondi con un valore diverso da text/event-stream Content-Type o restituisci uno stato HTTP diverso da 200 OK (ad esempio 404 Not Found).

Entrambi i metodi impediscono al browser di ristabilire la connessione.

Qualche parola sulla sicurezza

Le richieste generate da EventSource sono soggette ai criteri della stessa origine di altre API di rete come fetch. Se vuoi che l'endpoint SSE sul tuo server sia accessibile da origini diverse, scopri come attivarlo con la condivisione delle risorse tra origini (CORS).