Спецификация 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
, затем пока-пока
и разрывает соединение.
После этого браузер автоматически переподключается.
let http = require('http');
let url = require('url');
let querystring = require('querystring');
function onDigits(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache'
});
let i = 0;
let timer = setInterval(write, 1000);
write();
function write() {
i++;
if (i == 4) {
res.write('event: bye\ndata: пока-пока\n\n');
clearInterval(timer);
res.end();
return;
}
res.write('data: ' + i + '\n\n');
}
}
function accept(req, res) {
if (req.url == '/digits') {
onDigits(req, res);
return;
}
fileServer.serve(req, res);
}
if (!module.parent) {
http.createServer(accept).listen(8080);
} else {
exports.accept = accept;
}
<!DOCTYPE html>
<script>
let eventSource;
function start() { // когда нажата кнопка "Старт"
if (!window.EventSource) {
// Internet Explorer или устаревшие браузеры
alert("Ваш браузер не поддерживает EventSource.");
return;
}
eventSource = new EventSource('digits');
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 + "<br>";
document.documentElement.scrollTop = 99999999;
}
</script>
<button onclick="start()">Старт</button> Нажмите кнопку "Старт" для начала
<div id="logElem" style="margin: 6px 0"></div>
<button onclick="stop()">Стоп</button> Чтобы закончить, нажмите "Стоп".
Итого
Объект 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 обычно ставят в конце.