Потоковое обновление с событиями, отправленными сервером

События, отправленные сервером (SSE), отправляют автоматические обновления клиенту с сервера через HTTP-соединение. После установления соединения серверы могут инициировать передачу данных.

Возможно, вы захотите использовать SSE для отправки push-уведомлений из вашего веб-приложения. SSE отправляют информацию в одном направлении, поэтому вы не будете получать обновления от клиента.

Концепция SSE может быть вам знакома. Веб-приложение «подписывается» на поток обновлений, генерируемых сервером, и при возникновении нового события клиенту отправляется уведомление. Но чтобы по-настоящему понять события, отправляемые сервером, нам необходимо понять ограничения его предшественников AJAX. Это включает в себя:

  • Опрос : приложение неоднократно опрашивает сервер на наличие данных. Этот метод используется большинством приложений AJAX. В протоколе HTTP получение данных основано на формате запроса и ответа. Клиент делает запрос и ждет, пока сервер ответит данными. Если ни один из них не доступен, возвращается пустой ответ. Дополнительный опрос приводит к увеличению накладных расходов HTTP.

  • Длинный опрос (зависание GET/COMET) : если на сервере нет доступных данных, сервер сохраняет запрос открытым до тех пор, пока не станут доступны новые данные. Следовательно, этот метод часто называют «зависающим GET». Когда информация становится доступной, сервер отвечает, закрывает соединение, и процесс повторяется. Таким образом, сервер постоянно отвечает новыми данными. Чтобы настроить это, разработчики обычно используют хаки, такие как добавление тегов скриптов к «бесконечному» iframe.

События, отправляемые сервером, были разработаны с нуля, чтобы быть эффективными. При обмене данными с SSE сервер может передавать данные в ваше приложение, когда захочет, без необходимости делать первоначальный запрос. Другими словами, обновления могут передаваться с сервера на клиент по мере их возникновения. SSE открывают единый однонаправленный канал между сервером и клиентом.

Основное различие между событиями, отправленными сервером, и длинным опросом заключается в том, что SSE обрабатываются непосредственно браузером, и пользователю просто нужно прослушивать сообщения.

События, отправленные сервером, и WebSockets

Почему вы предпочитаете события, отправляемые сервером, вместо WebSockets? Хороший вопрос.

WebSockets имеет богатый протокол с двунаправленной полнодуплексной связью. Двусторонний канал лучше подходит для игр, приложений для обмена сообщениями и в любых случаях, когда вам нужны обновления практически в реальном времени в обоих направлениях.

Однако иногда вам нужна только односторонняя связь с сервером. Например, когда друг обновляет свой статус, биржевые котировки, ленты новостей или другие механизмы автоматической отправки данных. Другими словами, обновление клиентской базы данных Web SQL или хранилища объектов IndexedDB. Если вам нужно отправить данные на сервер, XMLHttpRequest всегда будет вашим другом.

SSE отправляются через HTTP. Для работы не требуется специального протокола или реализации сервера. Для работы с протоколом WebSocket требуются полнодуплексные соединения и новые серверы WebSocket.

Кроме того, события, отправляемые сервером, обладают множеством функций, которых нет в WebSockets, включая автоматическое переподключение, идентификаторы событий и возможность отправлять произвольные события.

Создайте источник событий с помощью JavaScript

Чтобы подписаться на поток событий, создайте объект EventSource и передайте ему URL-адрес вашего потока:

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

Затем настройте обработчик события message . Вы можете дополнительно прослушивать open и 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.
  }
});

Когда обновления отправляются с сервера, срабатывает обработчик onmessage , и новые данные становятся доступными в его свойстве e.data . Самое волшебное заключается в том, что всякий раз, когда соединение закрывается, браузер автоматически повторно подключается к источнику примерно через 3 секунды. Реализация вашего сервера может даже контролировать этот тайм-аут повторного подключения .

Вот и все. Теперь ваш клиент может обрабатывать события stream.php .

Формат потока событий

Отправка потока событий из источника — это вопрос создания простого текстового ответа, обслуживаемого с типом контента text/event-stream , который соответствует формату SSE. В своей базовой форме ответ должен содержать строку data: за которой следует ваше сообщение, а затем два символа «\n» для завершения потока:

data: My message\n\n

Многострочные данные

Если ваше сообщение длиннее, вы можете разбить его, используя несколько строк data: :. Две или более последовательные строки, начинающиеся с data: обрабатываются как один фрагмент данных, то есть генерируется только одно событие message .

Каждая строка должна заканчиваться одним символом «\n» (кроме последней, которая должна заканчиваться двумя). Результат, передаваемый обработчику message , представляет собой одну строку, объединенную символами новой строки. Например:

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

Это создает «первую строку\nвторую строку» в e.data . Затем можно было бы использовать e.data.split('\n').join('') для восстановления сообщения без символов "\n".

Отправить данные JSON

Использование нескольких строк помогает отправлять JSON, не нарушая синтаксис:

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

И возможный клиентский код для обработки этого потока:

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

Связать идентификатор с событием

Вы можете отправить уникальный идентификатор вместе с событием потока, включив строку, начинающуюся с id: :

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

Установка идентификатора позволяет браузеру отслеживать последнее запущенное событие, поэтому в случае разрыва соединения с сервером для нового запроса устанавливается специальный HTTP-заголовок ( Last-Event-ID ). Это позволяет браузеру определить, какое событие подходит для запуска. Событие message содержит свойство e.lastEventId .

Управление таймаутом повторного подключения

Браузер пытается повторно подключиться к источнику примерно через 3 секунды после закрытия каждого соединения. Вы можете изменить этот тайм-аут, включив строку, начинающуюся с retry: , за которой следует количество миллисекунд ожидания перед попыткой повторного подключения.

В следующем примере предпринимается попытка повторного подключения через 10 секунд:

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

Укажите название события

Один источник событий может генерировать события разных типов, включая имя события. Если присутствует строка, начинающаяся с event: за которой следует уникальное имя события, событие связано с этим именем. На клиенте можно настроить прослушиватель событий для прослушивания этого конкретного события.

Например, следующий вывод сервера отправляет три типа событий: общее событие «сообщение», событие «вход в систему» ​​и событие «обновление»:

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

При настройке прослушивателей событий на клиенте:

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

Примеры серверов

Вот базовая реализация сервера на 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()));
?>

Вот аналогичная реализация на Node JS с использованием обработчика 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>

Отменить трансляцию событий

Обычно браузер автоматически повторно подключается к источнику событий, когда соединение закрывается, но это поведение можно отменить как на клиенте, так и на сервере.

Чтобы отменить поток от клиента, вызовите:

source.close();

Чтобы отменить поток с сервера, ответьте, указав Content-Type отличный от text/event-stream , или верните статус HTTP, отличный от 200 OK (например, 404 Not Found ).

Оба метода не позволяют браузеру восстановить соединение.

Несколько слов о безопасности

На запросы, генерируемые EventSource, распространяются те же политики происхождения, что и на другие сетевые API, такие как выборка. Если вам нужно, чтобы конечная точка SSE на вашем сервере была доступна из разных источников, прочтите, как включить совместное использование ресурсов между источниками (CORS) .