Пошук уроків, статей та іншого контенту
Навчить реалізовувати односторонню доставку подій від сервера до клієнта через Server-Sent Events.
Server-Sent Events (SSE) — це механізм односторонньої доставки подій від сервера до браузера через звичайне HTTP-з’єднання.
Клієнт відкриває довготривале з’єднання, а сервер надсилає події в міру їх появи. На відміну від звичайного HTTP-запиту, сервер не закриває з’єднання після першої відповіді.
SSE підходить для:
сповіщень;
оновлення статусу довгих операцій;
прогресу виконання задач;
стрічок подій;
оновлення даних у реальному часі;
моніторингу стану сервісів.
SSE працює лише в одному напрямку:
сервер ───────────────> клієнтКлієнт може відкрити з’єднання та отримувати події, але сервер не отримує повідомлення через цей самий канал. Для надсилання даних від клієнта до сервера використовують звичайні HTTP-запити.
Клієнт створює об’єкт EventSource:
const source = new EventSource("/events");Сервер відповідає спеціальним типом вмісту:
Content-Type: text/event-streamПісля цього сервер надсилає події у спеціальному текстовому форматі:
event: message
id: 42
data: {"text":"Нове повідомлення"}
Порожній рядок завершує одну подію. Саме після порожнього рядка браузер передає подію обробнику.
Основні поля SSE:
event — назва події;
data — дані події;
id — ідентифікатор події;
retry — затримка перед повторною спробою підключення в мілісекундах.
Найчастіше дані передають як JSON:
event: update
data: {"status":"completed"}
Нижче наведено повний приклад на Node.js без додаткових бібліотек. Сервер:
віддає HTML-сторінку;
створює SSE-з’єднання на маршруті /events;
надсилає подію кожні дві секунди;
підтримує повторне підключення клієнта за допомогою Last-Event-ID.
Створіть файл server.js:
const http = require("node:http");
const clients = new Set();
let nextEventId = 1;
function sendEvent(response, event) {
response.write(`id: ${event.id}\n`);
response.write(`event: ${event.type}\n`);
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
}
function createEvent() {
return {
id: nextEventId++,
type: "update",
data: {
message: "Оновлення від сервера",
timestamp: new Date().toISOString()
}
};
}
const html = `<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Server-Sent Events</title>
</head>
<body>
<h1>Події від сервера</h1>
<p id="status">Підключення...</p>
<ul id="events"></ul>
<script>
const statusElement = document.querySelector("#status");
const eventsElement = document.querySelector("#events");
const source = new EventSource("/events");
source.addEventListener("open", () => {
statusElement.textContent = "З'єднання встановлено";
});
source.addEventListener("update", (event) => {
const data = JSON.parse(event.data);
const item = document.createElement("li");
item.textContent = data.message + " — " + data.timestamp;
eventsElement.prepend(item);
});
source.addEventListener("error", () => {
statusElement.textContent = "З'єднання перервано, повторна спроба...";
});
</script>
</body>
</html>`;
const server = http.createServer((request, response) => {
if (request.url === "/") {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8"
});
response.end(html);
return;
}
if (request.url === "/events") {
response.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
});
response.write(": з'єднання встановлено\n\n");
const lastEventIdHeader = request.headers["last-event-id"];
const lastEventId = Number(lastEventIdHeader || 0);
const client = {
response,
lastEventId
};
clients.add(client);
request.on("close", () => {
clients.delete(client);
});
return;
}
response.writeHead(404, {
"Content-Type": "text/plain; charset=utf-8"
});
response.end("Сторінку не знайдено");
});
setInterval(() => {
const event = createEvent();
for (const client of clients) {
if (event.id > client.lastEventId) {
sendEvent(client.response, event);
client.lastEventId = event.id;
}
}
}, 2000);
server.listen(3000, () => {
console.log("Сервер запущено: http://localhost:3000");
});Запустіть сервер:
node server.jsВідкрийте в браузері:
http://localhost:3000Кожні дві секунди сторінка отримуватиме нову подію без додаткових запитів із клієнта.
Для SSE важливі такі заголовки:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-aliveContent-TypeЗначення text/event-stream повідомляє браузеру, що відповідь має формат потоку SSE.
Cache-ControlПодії повинні надходити без кешування:
Cache-Control: no-cacheІнакше проміжний кеш може затримувати або повторно віддавати старі дані.
ConnectionЗ’єднання має залишатися відкритим:
Connection: keep-aliveУ різних HTTP-серверах і проксі підтримка цього заголовка може реалізовуватися автоматично, але SSE-відповідь все одно повинна бути потоковою.
Подія складається з одного або кількох полів і завершується порожнім рядком:
event: notification
id: 15
data: {"type":"success","message":"Операцію завершено"}
На клієнті така подія обробляється за її назвою:
const source = new EventSource("/events");
source.addEventListener("notification", (event) => {
const notification = JSON.parse(event.data);
console.log(notification.message);
});Якщо поле event не вказати, браузер використовує стандартну назву message:
data: {"message":"Звичайна подія"}
Обробник:
source.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data);
};Поле data може повторюватися кілька разів:
data: перший рядок
data: другий рядок
Браузер об’єднає ці рядки символом нового рядка. На практиці зручніше передавати один JSON-рядок, якщо подія має складну структуру.
EventSource автоматично намагається відновити з’єднання, якщо воно було перерване.
Сервер може вказати затримку повторного підключення:
retry: 5000
data: {"message":"Подія"}
У цьому прикладі браузер чекатиме приблизно 5 секунд перед новою спробою.
Клієнт може реагувати на стан з’єднання:
const source = new EventSource("/events");
source.onopen = () => {
console.log("SSE-з'єднання відкрите");
};
source.onerror = () => {
console.log("Помилка або розрив SSE-з'єднання");
};
source.onmessage = (event) => {
console.log(event.data);
};Подія error не обов’язково означає остаточну помилку. Якщо сервер знову стане доступним, EventSource зазвичай автоматично перепідключиться.
Щоб повністю припинити підключення, викликають:
source.close();Сервер може додавати до подій поле id:
id: 100
data: {"message":"Подія з ідентифікатором"}
Якщо з’єднання перерветься після отримання цієї події, під час повторного підключення браузер може надіслати серверу заголовок:
Last-Event-ID: 100Сервер може використати це значення, щоб продовжити доставку подій із потрібного місця.
Важливо: сам по собі id не зберігає події на сервері. Щоб відновити пропущені події, сервер повинен мати власне сховище або журнал подій.
У форматі SSE рядки, що починаються з двокрапки, є коментарями:
: перевірка з'єднання
Коментар не створює подію для JavaScript, але передає дані через мережу. Це можна використовувати як heartbeat — періодичний сигнал, який не дає проксі або інфраструктурі вважати з’єднання неактивним.
Приклад:
setInterval(() => {
for (const client of clients) {
client.response.write(": heartbeat\n\n");
}
}, 15000);У виробничому застосунку інтервал heartbeat потрібно підбирати з урахуванням тайм-аутів проксі та балансувальників.
Після перепідключення клієнт може отримати подію повторно, якщо сервер не зберігає та не перевіряє ідентифікатори.
Тому обробник на клієнті має бути готовим до повторної доставки. Наприклад, для подій, які не можна застосувати двічі, можна перевіряти їхній id:
let lastProcessedId = 0;
const source = new EventSource("/events");
source.addEventListener("update", (event) => {
const eventId = Number(event.lastEventId);
if (eventId <= lastProcessedId) {
return;
}
lastProcessedId = eventId;
const data = JSON.parse(event.data);
console.log("Оброблено:", data);
});Надійність такого підходу залежить від логіки конкретної події. Деякі операції мають бути ідемпотентними — повторне застосування не повинно ламати стан системи.
Звичайний HTTP-запит має короткий життєвий цикл:
клієнт -> запит -> сервер
клієнт <- відповідь <- серверSSE залишає відповідь відкритою:
клієнт -> відкриття з'єднання -> сервер
клієнт <- подія
клієнт <- подія
клієнт <- подіяКоли сервер більше не має надсилати події, він може завершити відповідь. Браузер сприйме це як розрив і спробує підключитися знову, якщо з’єднання не було закрито клієнтським кодом.
SSE добре підходить для односторонньої доставки, але має обмеження:
дані передаються лише від сервера до клієнта;
клієнт використовує довготривале HTTP-з’єднання;
потрібно правильно завершувати з’єднання після закриття сторінки;
проксі та балансувальники не повинні буферизувати події;
події мають бути текстовими;
автоматичне перепідключення може призводити до повторної доставки.
Якщо потрібен двосторонній канал у реальному часі, SSE не є відповідним транспортом. У цьому випадку застосовують інший протокол, наприклад WebSocket.
Content-TypeПомилка:
Content-Type: application/jsonДля SSE потрібно:
Content-Type: text/event-streamПомилка:
event: update
data: {"status":"ready"}Без порожнього рядка браузер може не передати подію обробнику.
Правильно:
event: update
data: {"status":"ready"}
Помилка:
response.write(`data: ${data}\n\n`);У результаті об’єкт може перетворитися на рядок [object Object].
Правильно:
response.write(`data: ${JSON.stringify(data)}\n\n`);Якщо сервер не видаляє закриті клієнтські з’єднання, колекція клієнтів зростатиме, а сервер продовжуватиме надсилати дані недоступним клієнтам.
Потрібно обробляти закриття запиту:
request.on("close", () => {
clients.delete(client);
});Деякі проксі можуть накопичувати відповідь і передавати її клієнту великими частинами. У такому разі події не надходитимуть одразу.
Для коректної роботи потрібно налаштувати проксі так, щоб він не буферизував SSE-потік.
JSON.parseevent.data завжди є рядком. Якщо сервер надсилає JSON, його потрібно розібрати:
const data = JSON.parse(event.data);Кожна вкладка браузера може створювати окреме SSE-з’єднання. Сервер має враховувати кількість одночасних клієнтів і правильно звільняти ресурси після їх відключення.
Типовий алгоритм складається з таких кроків:
Клієнт створює EventSource.
Сервер повертає заголовки text/event-stream і keep-alive.
Сервер зберігає відкрите з’єднання клієнта.
Коли з’являється нова подія, сервер серіалізує її в SSE-формат.
Сервер надсилає подію всім потрібним клієнтам.
Клієнт обробляє event.data.
Якщо з’єднання розірвалося, браузер повторює підключення.
Сервер за потреби використовує Last-Event-ID для відновлення доставки.
SSE — це механізм односторонньої доставки подій від сервера до клієнта.
На клієнті для SSE використовується EventSource.
Сервер має повертати Content-Type: text/event-stream.
Кожна подія завершується порожнім рядком.
Дані подій зазвичай передають у форматі JSON.
Поля event, id, data і retry керують поведінкою події.
EventSource автоматично намагається перепідключитися після розриву.
Last-Event-ID дає змогу серверу продовжити доставку після перепідключення.
Сервер повинен видаляти закриті з’єднання та враховувати можливість повторної доставки подій.