Специфікація 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
. - Щоб надіслати розрив рядка
\n
, ми можемо негайно надіслати ще однеdata:
(3-тє повідомлення вище).
На практиці складні повідомлення зазвичай надсилаються в кодуванні JSON. Розриви рядків у них кодуються як \n
, тому багаторядкові повідомлення data:
не потрібні.
Наприклад:
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:
може надсилатись як разом із деякими даними, так і окремим повідомленням.
Браузер повинен зачекати вказану кількість мілісекунд перед повторним з’єднанням. Або довше, напр. якщо браузер знає (з ОС), що на даний момент немає з’єднання із мережею, він може зачекати, доки з’єднання з’явиться, а потім повторити спробу.
- Якщо сервер бажає, щоб браузер припинив повторне з’єднання, він повинен відповісти HTTP статусом 204.
- Якщо браузер хоче закрити з’єднання, він повинен викликати
eventSource.close()
:
let eventSource = new EventSource(...);
eventSource.close();
Крім того, не буде повторного з’єднання, якщо відповідь містить неправильний Content-Type
або його статус HTTP відрізняється від 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
генерує три події:
message
– отримане повідомлення, доступне якevent.data
.open
– з’єднання відкрите.error
– не вдалося приєднатися, напр. сервер повернув статус HTTP 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
, потім bye
та розриває з’єднання.
Потім браузер автоматично відновить з’єднання.
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');
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: bye-bye\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) {
// для IE чи іншого старого браузера
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("eventSource.close()");
}
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
що налаштовується. - Ідентифікатори повідомлень для відновлення подій, останній отриманий id надсилається в заголовку
Last-Event-ID
після повторного з’єднання. - Поточний стан знаходиться у властивості
readyState
.
Це робить EventSource
життєздатною альтернативою WebSocket
, оскільки останній є більш низькорівневим і не має таких вбудованих функцій (хоча їх можна реалізувати).
У багатьох реальних програмах потужності EventSource
якраз достатньо.
Підтримується у всіх сучасних браузерах (не в IE).
Синтаксис такий:
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:
зазвичай йде останнім…
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)