Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
281 lines (181 sloc) 14.5 KB

Server Side Events -- события с сервера

Сразу заметим, что на текущий момент этот способ поддерживают все современные браузеры, кроме IE.

Современный стандарт Server-Sent Events позволяет браузеру создавать специальный объект EventSource, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных.

Он, по дизайну, может меньше, чем WebSocket'ы.

С другой стороны, Server Side Events проще в реализации, работают по обычному протоколу HTTP и сразу поддерживают ряд возможностей, которые для WebSocket ещё надо реализовать.

Поэтому в тех случаях, когда нужна преимущественно односторонняя передача данных от сервера к браузеру, они могут быть удачным выбором.

Получение сообщений

При создании объекта new EventSource(src) браузер автоматически подключается к адресу src и начинает получать с него события:

var eventSource = new EventSource("/events/subscribe");

eventSource.onmessage = function(e) {
  console.log("Пришло сообщение: " + e.data);
};

Чтобы соединение успешно открылось, сервер должен ответить с заголовком Content-Type: text/event-stream, а затем оставить соединение висящим и писать в него сообщения в специальном формате:

data: Сообщение 1

data: Сообщение 2

data: Сообщение 3
data: из двух строк
  • Каждое сообщение пишется после data:. Если после двоеточия есть пробел, то он игнорируется.

  • Сообщения разделяются двумя строками \n\n.

  • Если нужно переслать перевод строки, то сообщение разделяется. Каждая следующая строка пересылается отдельным data:.

    В частности, две последние строки в примере выше составляют одно сообщение: "Сообщение 3\nиз двух строк".

Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как \n.

Соответственно, многострочные данные будут пересылаться так:

data: {"user":"Вася","message":"Сообщение 3\n из двух строк"}

...То есть, строка data: будет одна, и никаких проблем с разделением сообщения нет.

Восстановление соединения

При создании объекта браузер автоматически подключается к серверу, а при обрыве -- пытается его возобновить.

Это очень удобно, никакой другой транспорт не обладает такой встроенной способностью.

При любом закрытии соединения, в том числе если сервер ответит на запрос и закроет соединение сам -- браузер через короткое время повторит свой запрос.

Есть лишь два способа, которыми сервер может "отшить" надоедливый `EventSource`:

- Ответить со статусом не 200.
- Ответить с `Content-Type`, не совпадающим с `text/event-stream`.

Между попытками возобновить соединение будет пауза, начальное значение которой зависит от браузера (1-3 секунды) и может быть изменено сервером через указание retry: в ответе:

retry: 15000
data: Поставлена задержка 15 секунд

Браузер, со своей стороны, может закрыть соединение вызовом close():

var eventSource = new EventSource(...);

eventSource.close();

При этом дальнейших попыток соединения не будет. Открыть обратно этот объект тоже нельзя, можно создать новый EventSource.

Идентификатор id

Для того, чтобы продолжить получение событий с места разрыва, стандарт предусматривает идентификацию событий через id.

Сервер может указать его в ответе:

data: Сообщение 1
id: 1

data: Сообщение 2
id: 2

data: Сообщение 3
data: из двух строк
id: 3

При получении id: браузер:

  • Устанавливает свойство eventSource.lastEventId в его значение.
  • При пересоединении пошлёт заголовок Last-Event-ID с этим id, так что сервер сможет переслать последующие, пропущенные, сообщения.

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

Статус соединения readyState

У объекта EventSource есть свойство readyState, которое содержит одно из значений (выдержка из стандарта):

const unsigned short CONNECTING = 0; // в процессе (пере-)соединения
const unsigned short OPEN = 1;       // соединение установлено
const unsigned short CLOSED = 2;     // соединение закрыто

При создании объекта и при разрыве оно автоматически равно CONNECTING.

События

Событий всего три:

  • onmessage -- пришло сообщение, доступно как event.data
  • onopen -- при успешном установлении соединения
  • onerror -- при ошибке соединения.

Например:

var eventSource = new EventSource('digits');

eventSource.onopen = function(e) {
  console.log("Соединение открыто");
};

eventSource.onerror = function(e) {
  if (this.readyState == EventSource.CONNECTING) {
    console.log("Соединение порвалось, пересоединяемся...");
  } else {
    console.log("Ошибка, состояние: " + this.readyState);
  }
};

eventSource.onmessage = function(e) {
  console.log("Пришли данные: " + e.data);
};

Своё имя события: event

По умолчанию на события срабатывает обработчик onmessage, но можно сделать и свои события. Для этого сервер должен указать перед событием его имя после event:.

Например:

event: join
data: Вася

data: Привет

event: leave
data: Вася

Сообщение по умолчанию имеет имя message.

Для обработки своих имён событий необходимо ставить обработчик при помощи addEventListener.

Пример кода для обработки:

eventSource.addEventListener('join', function(e) {
  alert( 'Пришёл ' + e.data );
});

eventSource.addEventListener('message', function(e) {
  alert( 'Сообщение ' + e.data );
});

eventSource.addEventListener('leave', function(e) {
  alert( 'Ушёл ' + e.data );
});

Демо

В примере ниже сервер посылает в соединение числа от 1 до 3, а затем -- событие bye и закрывает соединение. Браузер автоматически откроет его заново.

[codetabs src="eventsource"]

Кросс-доменность

EventSource поддерживает кросс-доменные запросы, аналогично XMLHttpRequest. Для этого у конструктора есть второй аргумент -- объект, который нужно передать так:

var source = new EventSource("http://pupkin.ru/stream", {
  withCredentials: true
});

Второй аргумент сделан объектом с расчётом на будущее. Пока что никаких других свойств там не поддерживается, только withCredentials.

Сервер при этом получит заголовок Origin с доменом запроса и должен ответить с заголовком Access-Control-Allow-OriginAccess-Control-Allow-Credentials, если стоит withCredentials), в точности как в главе info:xhr-crossdomain.

При кросс-доменных запросах у событий event также появится дополнительное свойство origin, содержащее адрес источника, откуда пришли данные. Его можно использовать для дополнительной проверки со стороны браузера:

eventSource.addEventListener('message', function(e) {
  if (e.origin != 'http://javascript.ru') return;
  alert( 'Сообщение ' + e.data );
});

Итого

Объект EventSource предназначен для передачи текстовых сообщений с сервера, используя обычный протокол HTTP.

Он предлагает не только передачу сообщений, но и встроенную поддержку важных вспомогательных функций:

  • События event.
  • Автоматическое пересоединение, с настраиваемой задержкой retry.
  • Проверка текущего состояния подключения по readyState.
  • Идентификаторы сообщений id для точного возобновления потока данных, последний полученный идентификатор передаётся в заголовке Last-Event-ID.
  • Кросс-доменность CORS.

Этот набор функций делает EventSource достойной альтернативой WebSocket, которые хоть и потенциально мощнее, но требуют реализации всех этих функций на клиенте и сервере, поверх протокола.

Поддержка -- все браузеры, кроме IE.

  • Синтаксис:

    var source = new EventSource(src[, credentials]); // src - адрес с любого домена

    Второй необязательный аргумент, если указан в виде { withCredentials: true }, инициирует отправку Cookie и данных авторизации при кросс-доменных запросах.

    Безопасность при кросс-доменных запросах обеспечивается аналогично XMLHttpRequest.

  • Свойства объекта:

    readyState : Текущее состояние соединения, одно из EventSource.CONNECTING (=0), EventSource.OPEN (=1) или EventSource.CLOSED (=2).

    lastEventId : Последнее полученное id, если есть. При возобновлении соединения браузер указывает это значение в заголовке Last-Event-ID.

    url, withCredentials : Параметры, переданные при создании объекта. Менять их нельзя.

  • Методы:

    close() : Закрывает соединение.

  • События:

    onmessage : При сообщении, данные -- в event.data.

    onopen : При установлении соединения.

    onerror : При ошибке, в том числе -- закрытии соединения по инициативе сервера.

    Эти события можно ставить напрямую через свойство: source.onmessage = ....

    Если сервер присылает имя события в event:, то такие события нужно обрабатывать через addEventListener.

  • Формат ответа сервера:

    Сервер присылает пустые строки, либо строки, начинающиеся с:

    • data: -- сообщение, несколько таких строк подряд склеиваются и образуют одно сообщение.
    • id: -- обновляет lastEventId.
    • retry: -- указывает паузу между пересоединениями, в миллисекундах. JavaScript не может указать это значение, только сервер.
    • event: -- имя события, должен быть перед data:.