Aggiornamenti dello streaming con eventi inviati dal server

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

Potresti voler utilizzare gli SSE per inviare notifiche push dalla tua app web. Gli SSE inviano le informazioni in una direzione, quindi non riceverai aggiornamenti dal client.

Il concetto di SSE potrebbe esserti familiare. Un'app web "si iscrive" a un flusso di aggiornamenti generato da un server e, ogni volta che si verifica un nuovo evento, viene inviata una notifica al client. Tuttavia, per comprendere appieno gli eventi inviati dal server, dobbiamo conoscere i limiti dei predecessori di AJAX. Include:

  • Sondaggio: l'applicazione esegue ripetutamente il polling di un server per individuare dati. Questa tecnica è utilizzata dalla maggior parte delle applicazioni AJAX. Con il protocollo HTTP, il recupero dei dati ruota intorno a un formato di richiesta e risposta. Il client invia una richiesta e attende che il server risponda con i dati. Se non è disponibile, viene restituita una risposta vuota. Un polling aggiuntivo crea un overhead maggiore per il protocollo HTTP.

  • Polling prolungato (Hanging GET / COMET): se il server non ha dati disponibili, mantiene la richiesta aperta finché non vengono resi disponibili nuovi dati. Pertanto, questa tecnica è spesso definita "Hanging GET". 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. A questo scopo, gli sviluppatori in genere usano tecniche di pirateria informatica, ad esempio l'aggiunta di tag script a un iframe "infinito".

Gli eventi inviati dal server sono stati progettati da zero per essere efficienti. Durante la comunicazione con gli SSE, un server può eseguire il push dei dati alla tua app ogni volta che lo desidera, senza dover effettuare una richiesta iniziale. In altre parole, gli aggiornamenti possono essere trasmessi in streaming da server a client. Gli SSE aprono un singolo canale unidirezionale tra server e client.

La differenza principale tra gli eventi inviati dal server e i sondaggi lunghi è che le SSE vengono gestite direttamente dal browser e l'utente deve solo ascoltare i messaggi.

Confronto tra eventi inviati dal server e WebSocket

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

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

Tuttavia, a volte hai bisogno solo di una comunicazione unidirezionale da un server. Ad esempio, quando un amico aggiorna il proprio stato, i codici titolo, i feed di notizie o altri meccanismi di push automatico dei dati. In altre parole, un aggiornamento di un database SQL web lato client o di un archivio di oggetti IndexedDB. Se devi inviare dati a un server, XMLHttpRequest è sempre amico.

Gli SSE vengono inviati tramite HTTP. Non esiste un'implementazione speciale di protocolli o server per iniziare a lavorare. WebSocket richiede connessioni full-duplex e nuovi server WebSocket per gestire il protocollo.

Inoltre, gli eventi inviati dal server hanno una serie di funzionalità non progettate in WebSocket, tra cui la riconnessione automatica, gli ID evento e la possibilità di inviare eventi arbitrari.

Creazione di un oggetto EventSource con JavaScript

Per iscriverti a uno stream di eventi, crea un oggetto EventSource e trasmettilo l'URL del tuo stream:

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

A questo punto, configura un gestore per l'evento message. Facoltativamente, puoi ascoltare 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 il server esegue il push degli aggiornamenti, viene attivato il gestore onmessage e i nuovi dati sono disponibili nella relativa proprietà e.data. La parte magica è che, ogni volta che la connessione viene chiusa, il browser si riconnette automaticamente all'origine dopo circa 3 secondi. L'implementazione del server può anche avere controllo su questo timeout della riconnessione.

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

Formato stream di eventi

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

data: My message\n\n

Dati multilinea

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 una singola "\n" (tranne l'ultima, che dovrebbe terminare con due). Il risultato passato al tuo gestore message è una singola stringa concatenata da caratteri di nuova riga. Ad esempio:

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

In questo modo diventa "prima riga\nseconda riga" in e.data. Si potrebbe quindi utilizzare e.data.split('\n').join('') per ricostruire il messaggio senza i caratteri "\n".

Invia dati JSON

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

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

E il possibile 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 di streaming includendo una riga che inizi 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, qualora la connessione al server venga interrotta, venga impostata un'intestazione HTTP speciale (Last-Event-ID) con la nuova richiesta. Ciò consente al browser di determinare l'evento appropriato da attivare. L'evento message contiene una proprietà e.lastEventId.

Controlla il timeout della riconnessione

Il browser tenta di riconnettersi all'origine circa 3 secondi dopo la chiusura di ogni connessione. Puoi modificare questo timeout includendo una riga che inizi con retry:, seguita dal numero di millisecondi di attesa prima di provare a riconnettersi.

Nell'esempio seguente viene tentata una riconnessione dopo 10 secondi:

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

Specifica un nome per l'evento

Una singola origine evento può generare diversi tipi di eventi includendo un nome dell'evento. Se è presente una riga che inizia con event:, seguita da un nome univoco dell'evento, l'evento viene associato a quel nome. Sul client, è possibile impostare un listener di eventi per ascoltare quell'evento specifico.

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

Di seguito è riportata 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 con un gestore Espresso:

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 lo stream di eventi

Normalmente, il browser si riconnette 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 Content-Type diverso da text/event-stream o restituisci uno stato HTTP diverso da 200 OK (ad esempio 404 Not Found).

Entrambi i metodi impediscono al browser di ristabilire la connessione.

Una parola sulla sicurezza

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