Спецификация 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). lastEventIdidпоследнего полученного сообщения. При переподключении браузер посылает его в заголовке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 обычно ставят в конце.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)