Server Sent Events

Спецификация 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: в две строки

На практике сложные сообщения обычно отправляются в формате 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: может посылаться как вместе с данными, так и отдельным сообщением.

Браузеру следует ждать именно столько миллисекунд перед новой попыткой подключения. Или дольше, например, если браузер знает (от операционной системы) что соединения с сетью нет, то он может осуществить переподключение только когда оно появится.

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:, браузер:

Указывайте id: после data:

Обратите внимание: id указывается сервером после данных data сообщения, чтобы обновление lastEventId произошло после того, как сообщение будет получено.

Статус подключения: readyState

У объекта EventSource есть свойство readyState, имеющее одно из трёх значений:

EventSource.CONNECTING = 0; // подключение или переподключение
EventSource.OPEN = 1;       // подключено
EventSource.CLOSED = 2;     // подключение закрыто

При создании объекта и разрыве соединения оно автоматически устанавливается в значение EventSource.CONNECTING (равно 0).

Мы можем обратиться к этому свойству, чтобы узнать текущее состояние EventSource.

Типы событий

По умолчанию объект EventSource генерирует 3 события:

Сервер может указать другой тип события с помощью 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, затем пока-пока и разрывает соединение.

После этого браузер автоматически переподключается.

Сервер

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

for ($i=1;$i<4;$i++) { 
  echo "data: $i\n\n";
  sleep(1);
  ob_flush();
  flush();
  }
echo "event: bye\ndata: пока-пока\n\n";
  ob_flush();
  flush();
die();
?>
<!DOCTYPE html>
<html>
<head>
<script>
 var eventSource, logElem;
 window.onload = function() {  
   logElem = document.getElementById("logElem");
  }
</script>
</head>
<body>
<div id="logElem"></div>
<p><button onclick="start()">Старт</button> Нажмите кнопку "Старт" для начала</p>
<p><button onclick="stop()">Стоп</button> Чтобы закончить, нажмите "Стоп"</p>
<p><button onclick="logElem.innerHTML=''">Очистить «Лог сообщений»</button></p>
</body>
</html>
function start() { // когда нажата кнопка "Старт"
  if (!window.EventSource) {
    // Internet Explorer или устаревшие браузеры
    alert("Ваш браузер не поддерживает EventSource.");
    return;
  }

  eventSource = new EventSource('sse.php');

  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 + "
"; document.documentElement.scrollTop = 99999999; }
body {
  font-family: Verdana;
  font-size: 12px;
}

#logElem {
  width: 350px;
  height: 230px;
  border: darkgray 2px solid;
  border-radius: 5px;
  margin: 20px;
  padding: 10px;
  overflow: scroll;
  overflow-x: hidden;
}
p {margin: 5px 20px;}

Итого

Объект EventSource автоматически устанавливает постоянное соединение и позволяет серверу отправлять через него сообщения.

Он предоставляет:

Это делает 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.

Сообщение состоит из следующих полей:

Сообщение может включать одно или несколько этих полей в любом порядке, но id обычно ставят в конце.

Справочник JavaScript
×
Справочник JavaScript