Использование отправляемых сервером событий
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since January 2020.
Разрабатывать веб-приложения, использующие отправляемые сервером события (англ. Server-sent events, SSE) не сложно. Требуется немного кода на стороне сервера для передачи событий веб-приложению, а клиентская часть кода для обработки этих событий работает почти идентично веб-сокетам. Это одностороннее соединение, поэтому нельзя отправлять события от клиента на сервер.
Получение событий от сервера
API отправляемые сервером событий содержит в себе интерфейс EventSource
.
Создание экземпляра EventSource
Чтобы открыть соединение с сервером и начать получать от него события, необходимо создать новый объект EventSource
с URL-адресом скрипта, который генерирует события.
Например:
const evtSource = new EventSource("sse-demo.php");
Если скрипт генератора событий размещён на другом домене, необходимо создать новый объект EventSource
с URL и словарем параметров.
Предположим, что скрипт клиента находится на example.com
:
const evtSource = new EventSource("//api.example.com/sse-demo.php", {
withCredentials: true,
});
Подписка на события message
Сообщения, отправленные с сервера и не имеющие поля event
, принимаются как события message
.
Чтобы получать сообщения из событий, необходимо установить обработчик для события message
:
evtSource.onmessage = (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = `Сообщение: ${event.data}`;
eventList.appendChild(newElement);
};
Этот код обрабатывает входящие события и добавляет текст сообщения в список в HTML-документе.
Подписка на пользовательские события
Сообщения от сервера, у которых определено поле event
, принимаются как события с именем, указанным в event
.
Например:
evtSource.addEventListener("ping", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
const time = JSON.parse(event.data).time;
newElement.textContent = `ping в ${time}`;
eventList.appendChild(newElement);
});
Этот код будет вызываться каждый раз, когда сервер отправляет сообщение с полем event
, установленным в значение ping
.
После этого он анализирует JSON в поле data
и выводит эту информацию.
Предупреждение:Без использования HTTP/2, максимальное количество открытых SSE-соединений может быть ограничено, что может быть особенно заметным при открытии нескольких вкладок, поскольку ограничение действует на браузер и установлено в очень низкое значение (6). Эта проблема отмечена как «Не будет исправлена» в Chrome и Firefox. Ограничение действует на связку «браузер + домен», то есть можно открыть только 6 SSE-соединений к www.example1.com
для всех вкладок и ещё 6 SSE-соединений к www.example2.com
(согласно StackOverflow). При использовании HTTP/2, максимальное количество одновременных HTTP-потоков согласовывается между сервером и клиентом (по умолчанию оно равно 100).
Отправка событий с сервера
Скрипт на стороне сервера, который отправляет события, должен отвечать с использованием MIME-типа text/event-stream
.
Каждое сообщение отправляется как блок текста, завершающийся парой пустых строк.
Подробнее о формате потока событий смотрите в Формат потока событий.
Ниже приведен код на языке PHP для примера, который мы использовали выше:
date_default_timezone_set("Europe/Moscow");
header("X-Accel-Buffering: no");
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
$counter = rand(1, 10);
while (true) {
// Отправляем событие "ping" каждую секунду
echo "event: ping\n";
$curDate = date(DATE_ISO8601);
echo 'data: {"time": "' . $curDate . '"}';
echo "\n\n";
// Отправляем простые сообщения со случайным интервалом
$counter--;
if (!$counter) {
echo 'Информация: это сообщение было отправлено в ' . $curDate . "\n\n";
$counter = rand(1, 10);
}
if (ob_get_contents()) {
ob_end_flush();
}
flush();
// Прерываем цикл если клиент закрыл соединение (закрытие страницы)
if (connection_aborted()) break;
sleep(1);
}
Приведенный выше код генерирует событие с типом «ping» каждую секунду. Данные каждого события представляют собой объект JSON, содержащий временную метку ISO 8601, соответствующую времени, в которое было сгенерировано событие. Через случайные интервалы времени отправляется простое сообщение (без типа события). Цикл будет продолжать работать независимо от состояния соединения, поэтому добавлена проверка, чтобы прервать цикл, если соединение было закрыто (например, при закрытии страницы).
Примечание: Полный код этого примера можно найти на GitHub, смотрите Simple SSE demo using PHP.
Обработка ошибок
При возникновении ошибок (например, проблемы сети или доступа), генерируется сообщение об ошибке. Его можно обработать программно, установив метод onerror
в объекте EventSource
:
evtSource.onerror = (err) => {
console.error("В EventSource произошла ошибка:", err);
};
Закрытие потока событий
По умолчанию, если соединение между клиентом и сервером закрывается, оно будет перезапущено. Для завершения соединения необходимо вызывать метод .close()
.
evtSource.close();
Формат потока событий
Поток событий — это поток текстовых данных, которые должны быть закодированы с использованием UTF-8. Сообщения в потоке событий разделяются парой символов новой строки. Двоеточие в качестве первого символа строки по сути является комментарием и игнорируется.
Примечание: Строку комментариев можно использовать для предотвращения закрытия соединения. Сервер может периодически отправлять комментарии, чтобы поддерживать соединение активным.
Каждое сообщение состоит из одной или нескольких строк текста, содержащих поля этого сообщения. Каждое поле представлено именем, за которым следует двоеточие и текстовые данные для значения этого поля.
Поля
Каждое полученное сообщение содержит комбинацию следующих полей, по одному в каждой строке:
event
-
Строка, определяющая тип события. Если это поле указано, то событие будет передано браузером в обработчик события такого типа. Код на клиенте должен использовать
addEventListener()
для подписки на именованные события. Если имя события не было указано, то оно попадёт в обработчикonmessage
. data
-
Поле данных сообщения. Когда
EventSource
получает несколько строк подряд, которые начинаются сdata:
, он объединяет их, вставляя между ними символ переноса строка. Завершающие символы новой строки удаляются. id
-
Идентификатор события для установки значения последнего события в объекте
EventSource
. retry
-
Время переподключения. Если соединение с сервером потеряно, браузер будет ждать указанное время перед попыткой переподключения. Это должно быть целое число, указывающее время переподключения в миллисекундах. Если указано нецелое значение, поле игнорируется.
Другие имена полей игнорируются.
Примечание: Если строка не содержит двоеточия, вся строка рассматривается как имя поля с пустым значением.
Примеры
Сообщения, содержащие только данные
В следующем примере отправляется три сообщения. Первое — просто комментарий, так как начинается с двоеточия. Как упоминалось ранее, это может быть полезно для реализации механизма поддержания активности, если сообщения могут отправляться нерегулярно.
Второе сообщение содержит поле данных со значением some text
.
Третье сообщение содержит поле данных со значением another message\nwith two lines
. Обратите внимание на специальный символ новой строки в значении.
: this is a test stream
data: some text
data: another message
data: with two lines
Именованные события
В этом примере отправляются именованные события. Каждое из них имеет имя, указанное в поле event
, и поле data
, содержащее строку JSON с данными, необходимыми клиенту для выполнения действия по этому событию. Поле data
может содержать любые строковые данные, это не обязательно должен быть JSON.
event: userconnect
data: {"username": "vasya", "time": "02:33:48"}
event: usermessage
data: {"username": "vasya", "time": "02:34:11", "text": "Всем привет!"}
event: userdisconnect
data: {"username": "vasya", "time": "02:34:23"}
event: usermessage
data: {"username": "masha", "time": "02:34:36", "text": "Пока, vasya!"}
Смешивание и сопоставление
Не обязательно использовать только именованные или неименованные сообщения, можно объединить их в один поток событий.
event: userconnect
data: {"username": "vasya", "time": "02:33:48"}
data: Системное сообщение, которое будет использоваться
data: для выполнения какой-то задачи.
event: usermessage
data: {"username": "vasya", "time": "02:34:11", "text": "Всем привет!"}
Совместимость с браузерами
BCD tables only load in the browser