- Операторы
- Управляющие инструкции
- JS Объекты
- браузер BOM
- HTML DOM
- События
- HTML Объекты
- Промисы, async/await
- Сетевые запросы
- XMLHttpRequest
- Объекты URL
- Объект formData
- Fetch API
- Fetch API 2
- WebSocket
- Server Sent Events
- Бинарные данные и файлы
- Модули
- Классы
- Разное
Server Sent Events
Спецификация Server-Sent Events описывает встроенный класс EventSource
, который позволяет поддерживать соединение с сервером и получать от него события.
Как и в случае с WebSocket
, соединение постоянно.
Но есть несколько важных различий:
WebSocket |
EventSource |
---|---|
Двунаправленность: и сервер, и клиент могут обмениваться сообщениями | Однонаправленность: данные посылает только сервер |
Бинарные и текстовые данные | Только текст |
Протокол WebSocket | Обычный HTTP |
EventSource
не настолько мощный способ коммуникации с сервером, как WebSocket
.
Зачем нам его использовать?
Основная причина: он проще. Многим приложениям не требуется вся мощь WebSocket
.
Если нам нужно получать поток данных с сервера: неважно, сообщения в чате или же цены для магазина – с этим легко справится EventSource
. К тому же, он поддерживает автоматическое переподключение при потере соединения, которое, используя WebSocket
, нам бы пришлось реализовывать самим. Кроме того, используется старый добрый HTTP, а не новый протокол.
Получение сообщений
Чтобы начать получать данные, нам нужно просто создать new EventSource(url)
.
Браузер установит соединение с url
и будет поддерживать его открытым, ожидая события.
Сервер должен ответить со статусом 200 и заголовком Content-Type: text/event-stream
, затем он должен поддерживать соединение открытым и отправлять сообщения в особом формате:
data: Сообщение 1 data: Сообщение 2 data: Сообщение 3 data: в две строки
- Текст сообщения указывается после
data:
, пробел после двоеточия необязателен. - Сообщения разделяются двойным переносом строки
\n\n
. - Чтобы разделить сообщение на несколько строк, мы можем отправить несколько
data:
подряд (третье сообщение).
На практике сложные сообщения обычно отправляются в формате JSON, в котором перевод строки кодируется как \n
, так что в разделении сообщения на несколько строк обычно нет нужды.
Например:
data: {"user":"Джон","message":"Первая строка\n Вторая строка"}
…Так что можно считать, что в каждом data:
содержится ровно одно сообщение.
Для каждого сообщения генерируется событие message
:
let eventSource = new EventSource("/events/subscribe"); eventSource.onmessage = function(event) { console.log("Новое сообщение", event.data); // этот код выведет в консоль 3 сообщения, из потока данных выше }; // или eventSource.addEventListener('message', ...)
Кросс-доменные запросы
EventSource
, как и fetch
, поддерживает кросс-доменные запросы. Мы можем использовать любой URL:
let source = new EventSource("https://another-site.com/events");
Сервер получит заголовок Origin
и должен будет ответить с заголовком Access-Control-Allow-Origin
.
Чтобы послать авторизационные данные, следует установить дополнительную опцию withCredentials
:
let source = new EventSource("https://another-site.com/events", { withCredentials: true });
Более подробное описание кросс-доменных заголовков вы можете прочитать Fetch: запросы на другие сайты.
Переподключение
После создания new EventSource
подключается к серверу и, если соединение обрывается, – переподключается.
Это очень удобно, так как нам не приходится беспокоиться об этом.
По умолчанию между попытками возобновить соединение будет небольшая пауза в несколько секунд.
Сервер может выставить рекомендуемую задержку, указав в ответе retry:
(в миллисекундах):
retry: 15000 data: Привет, я выставил задержку переподключения в 15 секунд
Поле retry:
может посылаться как вместе с данными, так и отдельным сообщением.
Браузеру следует ждать именно столько миллисекунд перед новой попыткой подключения. Или дольше, например, если браузер знает (от операционной системы) что соединения с сетью нет, то он может осуществить переподключение только когда оно появится.
- Если сервер хочет остановить попытки переподключения, он должен ответить со статусом 204.
- Если браузер хочет прекратить соединение, он может вызвать
eventSource.close()
:
let eventSource = new EventSource(...); eventSource.close();
Также переподключение не произойдёт, если в ответе указан неверный Content-Type
или его статус отличается от 301, 307, 200 и 204. Браузер создаст событие "error"
и не будет восстанавливать соединение.
После того как соединение
окончательно закрыто, «переоткрыть» его уже нельзя. Если необходимо
снова подключиться, просто создайте новый EventSource
.
Идентификатор сообщения
Когда соединение прерывается из-за проблем с сетью, ни сервер, ни клиент не могут быть уверены в том, какие сообщения были доставлены, а какие – нет.
Чтобы правильно возобновить подключение, каждое сообщение должно иметь поле id
:
data: Сообщение 1 id: 1 data: Сообщение 2 id: 2 data: Сообщение 3 data: в две строки id: 3
Получая сообщение с указанным id:
, браузер:
- Установит его значение свойству
eventSource.lastEventId
. - При переподключении отправит заголовок
Last-Event-ID
с этимid
, чтобы сервер мог переслать последующие сообщения.
Указывайте id:
после data:
Обратите внимание: id
указывается сервером после данных data
сообщения, чтобы обновление lastEventId
произошло после того, как сообщение будет получено.
Статус подключения: readyState
У объекта EventSource
есть свойство readyState
, имеющее одно из трёх значений:
EventSource.CONNECTING = 0; // подключение или переподключение EventSource.OPEN = 1; // подключено EventSource.CLOSED = 2; // подключение закрыто
При создании объекта и разрыве соединения оно автоматически устанавливается в значение EventSource.CONNECTING
(равно 0
).
Мы можем обратиться к этому свойству, чтобы узнать текущее состояние EventSource
.
Типы событий
По умолчанию объект EventSource
генерирует 3 события:
message
– получено сообщение, доступно какevent.data
.open
– соединение открыто.error
– не удалось установить соединение, например, сервер вернул статус 500.
Сервер может указать другой тип события с помощью event: ...
в начале сообщения.
Например:
event: join data: Боб data: Привет event: leave data: Боб
Чтобы начать слушать пользовательские события, нужно использовать addEventListener
, а не onmessage
:
eventSource.addEventListener('join', event => { alert(`${event.data} зашёл`); }); eventSource.addEventListener('message', event => { alert(`Сказал: ${event.data}`); }); eventSource.addEventListener('leave', event => { alert(`${event.data} вышел`); });
Полный пример
В этом примере сервер посылает сообщения 1
, 2
, 3
, затем пока-пока
и разрывает соединение.
После этого браузер автоматически переподключается.
Сервер
<?php header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); for ($i=1;$i<4;$i++) { echo "data: $i\n\n"; sleep(1); ob_flush(); flush(); } echo "event: bye\ndata: пока-пока\n\n"; ob_flush(); flush(); die(); ?>
<!DOCTYPE html> <html> <head> <script> var eventSource, logElem; window.onload = function() { logElem = document.getElementById("logElem"); } </script> </head> <body> <div id="logElem"></div> <p><button onclick="start()">Старт</button> Нажмите кнопку "Старт" для начала</p> <p><button onclick="stop()">Стоп</button> Чтобы закончить, нажмите "Стоп"</p> <p><button onclick="logElem.innerHTML=''">Очистить «Лог сообщений»</button></p> </body> </html>
function start() { // когда нажата кнопка "Старт" if (!window.EventSource) { // Internet Explorer или устаревшие браузеры alert("Ваш браузер не поддерживает EventSource."); return; } eventSource = new EventSource('sse.php'); eventSource.onopen = function(e) { log("Событие: open"); }; eventSource.onerror = function(e) { log("Событие: error"); if (this.readyState == EventSource.CONNECTING) { log(`Переподключение (readyState=${this.readyState})...`); } else { log("Произошла ошибка."); } }; eventSource.addEventListener('bye', function(e) { log("Событие: bye, данные: " + e.data); }); eventSource.onmessage = function(e) { log("Событие: message, данные: " + e.data); }; } function stop() { // когда нажата кнопка "Стоп" eventSource.close(); log("Соединение закрыто"); } function log(msg) { logElem.innerHTML += msg + "
"; document.documentElement.scrollTop = 99999999; }
body { font-family: Verdana; font-size: 12px; } #logElem { width: 350px; height: 230px; border: darkgray 2px solid; border-radius: 5px; margin: 20px; padding: 10px; overflow: scroll; overflow-x: hidden; } p {margin: 5px 20px;}
Итого
Объект EventSource
автоматически устанавливает постоянное соединение и позволяет серверу отправлять через него сообщения.
Он предоставляет:
- Автоматическое переподключение с настраиваемой
retry
задержкой. - Идентификаторы сообщений для восстановления соединения. Последний полученный идентификатор посылается в заголовке
Last-Event-ID
при пересоединении. - Текущее состояние, записанное в свойстве
readyState
.
Это делает EventSource
достойной альтернативой протоколу WebSocket
, который сравнительно низкоуровневый и не имеет таких встроенных возможностей (хотя их и можно реализовать).
Для многих приложений возможностей EventSource
вполне достаточно.
Поддерживается во всех современных браузерах (кроме Internet Explorer).
Синтаксис:
let source = new EventSource(url, [credentials]);
Второй аргумент – необязательный объект с одним свойством: { withCredentials: true }
. Он позволяет отправлять авторизационные данные на другие домены.
В целом, кросс-доменная безопасность реализована так же как в fetch
и других методах работы с сетью.
Свойства объекта EventSource
readyState
- Текущее состояние подключения:
EventSource.CONNECTING (=0)
,EventSource.OPEN (=1)
илиEventSource.CLOSED (=2)
. lastEventId
id
последнего полученного сообщения. При переподключении браузер посылает его в заголовкеLast-Event-ID
.
Методы
close()
- Закрывает соединение.
События
message
- Сообщение получено, переданные данные записаны в
event.data
. open
- Соединение установлено.
error
- В случае ошибки, включая как потерю соединения, так и другие ошибки в нём. Мы можем обратиться к свойству
readyState
, чтобы проверить, происходит ли переподключение.
Сервер может выставить собственное событие с помощью event:
. Такие события должны быть обработаны с помощью addEventListener
, а не on<event>
.
Формат ответа сервера
Сервер посылает сообщения, разделённые двойным переносом строки \n\n
.
Сообщение состоит из следующих полей:
data:
– тело сообщения, несколькоdata
подряд интерпретируются как одно сообщение, разделённое переносами строк\n
.id:
– обновляет свойствоlastEventId
, отправляемое вLast-Event-ID
при переподключении.retry:
– рекомендованная задержка перед переподключением в миллисекундах. Не может быть установлена с помощью JavaScript.event:
– имя пользовательского события, должно быть указано передdata:
.
Сообщение может включать одно или несколько этих полей в любом порядке, но id обычно ставят в конце.