Server-Sent Events или сокращенно SSE представляет еще одну технологией взаимодействия клиента и сервера, которая позволяет серверу отправлять сообщению клиенту. Стоит отметить, что в отличие от WebSockets, коммуникация через Server-Sent Events
является однонаправленной: сообщения доставляются в одном направлении - от сервера к клиенту (например, веб-браузеру пользователя).
Это делает их отличным выбором, когда нет необходимости отправлять данные от клиента на сервер. Например, Server-Sent Events
можно применять
для обработки таких вещей, как обновление статуса в социальных сетях, ленты новостей или отправка данных для хранения на стороне клиента.
На веб-странице в коде JavaScript для взаимодействия с сервером применяется интерфейс EventSource. Объкт EventSource по сути представляет собой сервер, который генерирует события или отправляет сообщения. Для создания объекта EventSource применяется конструктор:
new EventSource(url, options)
В качестве первого обязательного параметра в конструктор EventSource
передается URL-адрес ресурса на сервере:
const evtSource = new EventSource("/events");
Также опционально можно передать необязательный параметр, который настраивает объект EventSource. Этот параметр представляет объект с одним свойством
withCredentials. Это свойство указывает, следует ли включать заголовки CORS для кроссдоменного взаимодействия. По умолчвнию оно равно false
Для управления состояния подключением в EventSource определено ряд событий:
open: генерируется при установке соединения. Для установки обработчика события можно применять свойство onopen
error: генерируется при возникновении ошибки при установке соединения. Для установки обработчика события можно применять свойство onerror
message: генерируется при получении данных с сервера. Для установки обработчика события можно применять свойство onmessage
В качестве параметра обработчики этих событий принимают стандартный объект Event
. Пример установки обработчиков событий:
const evtSource = new EventSource("/events"); // с помощью addEventListener evtSource.addEventListener("open", () => { console.log("connection opened"); }); evtSource.addEventListener("error", () => { console.log("Error"); }); // с помощью свойств evtSource.onopen = () => { console.log("connection opened"); }; evtSource.onerror = () => { console.log("Error"); };
Когда приходят данные с сервера, у объекта WebSocket
срабатывает событие message, для установки обработчика которого можно использовать
свойство onmessage, либо метод addEventListener()
.
В обработчик события message передается объект типа MessageEvent. Этот объект предоставляет ряд свойств, которые позволяют извлечь данные ответа сервера:
data: возвращает полученные данные
origin: хранит адрес отправителя
lastEventId: хранит уникальный идентификатор последнего события в виде строки.
source: возвращает объект MessageEventSource
,
который может быть объектом WindowProxy, MessagePort или ServiceWorker и который представляет отправителя полученных данных.
ports: возвращает массив объектов MessagePort
, которые хранят использованные для отправки порты
Пример получения данных:
const evtSource = new EventSource("/events"); evtSource.onmessage = (event) => { console.log(event.data); // выводим отправленные данные на консоль };
Для закрытия соединения применяется метод close():
evtSource.close();
Рассмотрим небольшой пример взаимодействия между клиентом и сервером с помощью Server-Sent Events. В качестве клиента будет выступать код JavaScript на веб-странице. А в качестве сервера будем использовать Node.js.
Сначала определим код сервере. Для этого создадим файл server.js со следующим кодом:
const http = require("http"); const fs = require("fs"); // данные для отправки клиенту const messages = ["Привет", "Как дела?", "Что делаешь?", "Ты че спишь?", "Ну пока"]; http.createServer(function(request, response){ if(request.url == "/events"){ // если запрос SSE if (request.headers.accept && request.headers.accept === "text/event-stream") { sendEvent(response); } else{ response.writeHead(400); response.end("Bad Request"); } } else{ // в остальных случаях отправляем страницу index.html fs.readFile("index.html", (_, data) => response.end(data)); } }).listen(3000, ()=>console.log("Сервер запущен по адресу http://localhost:3000")); // отправляем сообщение клиенту function sendEvent(response) { // формируем заголовки response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); const id = (new Date()).toLocaleTimeString(); // определяем идентификатор последнего события // раз в 5 секунд отправляем одно сообщение setInterval(() => { createServerSendEvent(response, id); }, 5000); } // отправляем данные клиенту function createServerSendEvent(response, id) { // генерируем случайное число - индекс для массива messages const index = Math.floor(Math.random() * messages.length); const message = messages[index]; response.write("id: " + id + "\n"); response.write("data: " + message + "\n\n"); }
Вкратце пройдемся по коду. Сначала подключаются пакеты с функциональностью, которую мы собираемся использовать:
const http = require("http"); // для обработки входящих запросов const fs = require("fs"); // для чтения с жесткого диска файла index.html
Далее идент определение набора данных, которые будут отправляться клиенту - набор строк с важными сообщениями для клиента:
const messages = ["Привет", "Как дела?", "Что делаешь?", "Ты че спишь?", "Ну пока"];
Для создания сервера применяется функция http.createServer(). В эту функцию передается функция-обработчик, которая вызывается каждый раз, когда к серверу приходит запрос.
Эта функция имеет два параметра: request
(содержит данные запроса) и response
(управляет отправкой ответа).
В функции-обработчике с помощью свойства request.url
мы можем узнать, к какому ресурсу на сервере пришел запрос. Так, в данном случае,
если пришел запрос по пути "/events", то мы будем взаимодействовать с клиентом с помощью Server-Sent Events:
iif(request.url == "/events"){ // если запрос SSE if (request.headers.accept && request.headers.accept === "text/event-stream") { sendEvent(response); } else{ response.writeHead(400); response.end("Bad Request"); } }
И тут важно, чтобы в запросе были установлен заголовок "Accept": он должен иметь значение "text/event-stream". Если это так, то для отправки данных клиенту
выполняем функцию sendEvent()
, в которую передаем объект ответа response. Если же заголовки не установлены, то отправляем в ответ ошибку 400.
В функции sendEvent формируем заголовки ответа, получаем текущее время, которое будет выступать в качестве идентификатора события, и запускаем таймер с отправкой клиенту данных каждые 5 секунд.
function sendEvent(response) { // формируем заголовки response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); const id = (new Date()).toLocaleTimeString(); // определяем идентификатор последнего события // раз в 5 секунд отправляем одно сообщение setInterval(() => { createServerSendEvent(response, id); }, 5000); }
Собственно отправка данных производится в функции createServerSendEvent:
function createServerSendEvent(response, id) { // генерируем случайное число - индекс для массива messages const index = Math.floor(Math.random() * messages.length); const message = messages[index]; response.write("id: " + id + "\n"); response.write("data: " + message + "\n\n"); }
Здесь получаем случайное число, которое находится в диапазоне от 0 до messages.length и которое будет служить в качестве индекса, и по этому индексу выбирает некоторое сообщение. Далее формируем ответ. Устанавливаем идентификатор последнего события
response.write("id: " + id + "\n");
И устанавливаем собственно данные:
response.write("data: " + message + "\n\n");
Если запрос пришел на сервер по какому-то другому пути, то отправляем файл index.html, который мы дальше определим:
else{ fs.readFile("index.html", (_, data) => response.end(data)); }
Для считывания файлов применяется встроенная функция fs.readFile(). Первый параметр функции - адрес файла
(в данном случае предполагается, что файл index.html находится в одной папке с файлом сервера server.js). Второй параметр - функция, которая вызывается после считывания файла
и получет его содержимое через свой второй параметр data. Затем считанное содежимое также может быть отпавлено с помощью функции response.end(data)
.
В конце с помощью функции listen() запускаем веб-сервер на 3000 порту. То есть сервер будет запускаться по адресу http://localhost:3000/
Теперь в папке сервера определим простенький файл index.html со следующим кодом:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>METANIT.COM</title> </head> <body> <ul id="list"></ul> <script> const source = new EventSource("/events"); const list = document.getElementById("list") source.addEventListener("message", (e) => { const listItem = document.createElement("li"); listItem.textContent += e.data; list.appendChild(listItem); }); </script> </body> </html>
Здесь при получении данных от сервера добавляем их в список на веб-странице.
Теперь в консоли перейдем к папке сервера с помощью команды cd и запустим сервер с помощью команды node server.js
C:\app>node server.js Сервер запущен по адресу http://localhost:3000
После запуска сервера мы можем перейти в браузере по адресу http://localhost:3000, нам отобразится страница, в javascript-коде которой будет происходить получение данных от сервера и их вывод на веб-страницу: