Aggiornamenti dello streaming con eventi inviati dal server

Eric Bidelman

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

Potresti voler utilizzare SSE per inviare notifiche push dall'app web. Le SSE inviano le informazioni in una sola direzione, perciò non riceverai aggiornamenti di alto profilo.

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

  • Polling: l'applicazione esegue ripetutamente il polling su un server per raccogliere i dati. Questa tecnica è utilizzato dalla maggior parte delle applicazioni AJAX. Con il protocollo HTTP, il recupero I dati ruotano 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 visualizzato la risposta predefinita. Il polling aggiuntivo crea un maggiore overhead HTTP.

  • Sondaggio lungo (Hanging GET / COMET): se il server non dispone di dati la richiesta rimane aperta fino a quando non vengono resi disponibili nuovi dati. Pertanto, questa tecnica è spesso indicata come "GET pendente". Quando disponibili, il server risponde, chiude la connessione e il processo si ripete. Il server risponde quindi costantemente nuovi dati. Per configurarlo, gli sviluppatori in genere usano compromissioni come l'aggiunta di i tag di script in un intervallo iframe.

Gli eventi inviati dal server sono stati progettati da zero per essere efficienti. Durante la comunicazione con le SSE, un server può eseguire il push dei dati l'app in qualsiasi momento, senza bisogno di effettuare una richiesta iniziale. In altre parole, gli aggiornamenti possono essere trasmessi in flusso da server a client in tempo reale. SSE aprire un singolo canale unidirezionale tra server e client.

La differenza principale tra gli eventi inviati dal server e quelli con polling lungo è che le SSE sono 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é WebSocket? Ottima domanda.

WebSockets ha un protocollo avanzato con comunicazione bidirezionale e full-duplex. Un canale a doppia entrata è la scelta migliore giochi, app di messaggistica e qualsiasi caso d'uso per cui hai bisogno di 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, codici titolo, feed di notizie o altri meccanismi di push dei dati automatizzati. In altre parole, di un aggiornamento a un archivio di oggetti di un database SQL web o IndexedDB lato client. Se devi inviare dati a un server, XMLHttpRequest è sempre un amico.

Le SSE vengono inviate tramite HTTP. Non esiste un protocollo o un server speciale la tua implementazione per iniziare. I WebSocket richiedono l'architettura full-duplex e nuovi server WebSocket per gestire il protocollo.

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

Crea un EventSource con JavaScript

Per eseguire la sottoscrizione a un flusso di eventi, crea un oggetto EventSource e passalo URL del tuo stream:

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

A questo punto, configura un gestore per l'evento message. In via facoltativa, ascolta 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 viene eseguito il push degli aggiornamenti dal server, viene attivato il gestore onmessage e nuovi dati saranno disponibili nella relativa proprietà e.data. La parte magica è che ogni volta che la connessione viene chiusa, il browser si riconnette automaticamente al dopo circa 3 secondi. L'implementazione del server può anche avere il controllo il timeout della riconnessione.

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

Formato del flusso di eventi

L'invio di un flusso di eventi dall'origine consiste nel creare un risposta in testo normale, fornita con un Content-Type text/event-stream, che segue il formato SSE. Nella forma base, la risposta deve contenere una riga data:, seguita dalla seguito da due "\n" caratteri 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 di un singolo dato, il che significa che viene attivato un solo evento message.

Ogni riga deve terminare con un solo carattere "\n" (tranne l'ultima, che dovrebbe terminare con due). Il risultato passato al gestore message è una singola stringa concatenate da caratteri di nuova riga. Ad esempio:

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

Questo produce "prima riga\nseconda riga" a e.data. Si potrebbe quindi usare e.data.split('\n').join('') per ricostruire il messaggio "\n" caratteri.

Invia dati JSON

L'utilizzo di più righe ti aiuta a 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 di streaming 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 connessione al server viene interrotta, viene utilizzata un'intestazione HTTP speciale (Last-Event-ID) impostata con la nuova richiesta. Ciò consente al browser di determinare quale evento è appropriato attivare. L'evento message contiene una proprietà e.lastEventId.

Controllare il timeout della riconnessione

Il browser tenta di riconnettersi alla fonte per circa 3 secondi dopo la chiusura di ogni connessione. Puoi modificare il timeout includendo un parametro riga che inizia con retry:, seguita dal numero di millisecondi attendere prima di provare a riconnettersi.

Nell'esempio seguente, viene effettuato un tentativo di 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 nome dell'evento. Se è presente una riga che inizia con event:, seguito da un nome univoco per l'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 "messaggio" generico event, "userlogon" e "update" dell'evento:

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 la configurazione dei listener di eventi 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 utilizzando 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

Normalmente, il browser si riconnette automaticamente all'origine dell'evento quando la connessione viene chiuso, 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 messaggio diverso da text/event-stream Content-Type o restituisce 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 alle stesse norme di origine di con altre API di rete, come il fetch. Se hai bisogno che l'endpoint SSE sul tuo server accessibili da origini diverse, scopri come abilitarle Condivisione delle risorse tra origini (CORS).