Pull to refresh

Реализация HTTP server push с помощью Server-Sent Events

Reading time4 min
Views17K
На эту тему было уже много статей, но раскрыта далеко не вся правда. Для тех, кто пропустил — читайте Создание приложений реального времени с помощью Server-Sent Events .

Как же работает Server-Sent-Events?


Простой пример:
(new EventSource('/events')).addEventListener('message', function (e) {
document.getElementById('body').innerHTML += e.data + '
';
}, false);

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

Еще одним плюсом Server-sent-events является то, что последняя спецификация теперь поддерживается браузерами Chrome, Opera 11+, заявлена поддержка в Firefox Aurora, а для браузеров IE8+, Firefox 3.5+ можно реализовать polyfill на javascript, для более старых браузеров полифил может использовать long-polling. Таким образом, EventSource лучше поддерживается, нежели WebSockets. (Нативную поддержку смотрим здесь — http://caniuse.com/#search=eventsource)

При этом для работы полифила с серверной стороны не требуется подключение каких-то библиотек, достаточно реализовать работу с Server-sent-events, как этого требует стандарт, а не писать код под каждый ajax-транспорт (как это сделано в socket.io, что сильно усложняет ее, больше транспортов — больше тонкостей)

И так, IE8 поддерживает XDomainRequest, который может работать по технологии server-push, т.е. мы можем также получать сообщения, не прерывая http-соединения.
К сожалению, XDomainRequest требует паддинг — 2 килобайта в начале тела ответа, но это не проблема. Также, XDomainRequest не поддерживает установку заголовков запроса, поэтому Last-Event-ID придется передавать в теле запроса (POST).

Подробнее об XDomainRequest и его возможностях читать тут — http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx?PageIndex=1 )

Браузер Firefox позволяет обрабатывать сообщения с сервера по мере их поступления через стандартный XMLHttpRequest (см. статью javascript.ru/ajax/comet/xmlhttprequest-interactive)
Поэтому он также поддерживает «server-push».

Для остальных же браузеров, можно организовать получение сообщений по схеме long-polling с помощью XMLHttpRequest (т.е. с сервера нам необходимо для таких браузеров разрывать соединение после каждого отправленного сообщения).
Итак, готовый EventSource для браузеров, не имеющих его нативной поддержки — https://github.com/Yaffle/EventSource.
Зависимостей от библиотек — нет, все что нужно для работы:
<script type="text/javascript" src="eventsource.js"></script>

Проблемы использования server-push и long-polling


Браузеры ограничивают кол-во одновременных соединений, что при открытии сайта в нескольких вкладках браузера может вызывать проблемы. Конечно, в современных браузеров число соединений ограничено минимум шестью, но если каждая вкладка с вашим сайтом использует 2 или более длинных http-соединения, то этот предел быстро достигается.
Для решения этой проблемы как нельзя лучше подходит SharedWorker, создав SharedWorker мы можем создать внутри него EventSource и рассылать все события с объекта EventSource всем подключившимся скриптам:

Пример, sharedworker.js:
var es = new self.EventSource('events'),
    history = [];
es.addEventListener('message', function (e) {
  history.push(e.data);
}, false);
self.onconnect = function onConnect(event) {
  var port = event.ports[0]; // отсылаем все полученные ранее сообщения  
  history.forEach(function (data) {
    port.postMessage(data);
  });
  es.addEventListener('message', function (e) {
    port.postMessage(e.data);
  }, false);
}

Но, к сожалению, SharedWorker поддерживается только браузерами Opera 10.6+ и Chrome(Safari). При этом Opera не имеет объекта EventSource «внутри» SharedWorker (т.е. в SharedWorkerGlobalScope). Поэтому данный метод избежания нескольких соединений применим только для Webkit браузеров. Для всех остальных браузеров нам придется завести отдельный домен. Т.к. EventSource не поддерживает кросс-доменных запросов, я предлогаю подключать через iframe html-страницу, находящуюся на домене для EventSource, которая будет «запускать» EventSource и передавать сообщения через «postMessage» (недавно была статья на эту тему — http://habrahabr.ru/blogs/javascript/120336/, в которой описано использование библиотеки easyxdm, но «window.postMessage» поддерживается достаточно хорошо современными браузерами (http://caniuse.com/#search=postmessage).

Last-Event-ID


Когда Соединение с сервером разрывается (либо сервер закрыл соединение, либо произошла какая-то ошибка сети), EventSource выполняет повторное подключение через определенное время(это время можно контролировать). При этом новое подключение будет содержать в заголовке Last-Event-ID — идентификатор последнего полученного сообщения.
Рассмотрим пример ответа сервера — event stream:
retry: 1000\n
id: 123
data: hello world\n\n


Поле `retry` указывает серверу через какое кол-во милисекунд выполнять переподключение в случае разрыва соединения. Поле `id` будет передано в заголовке Last-Event-ID при переподключении. Поэтому с серверной стороны мы можем легко определить идентификатор последнего полученного пользователем сообщения и передать все накопившиемся после него. Пользователь получит все сообщения, ничего не пропустив. Для простоты в примере, отправка сообщения происходит методом GET без аутентификации и защиты от CSRF. Время в чате — местное для сервера, сорри.

Пример чата: http://hostel6.ru:8002

Исходники чата можно скачать тут: https://github.com/Yaffle/EventSource
Всем Спасибо за внимание!

UPD: Добавлена поддержка CORS для IE8+, FF4-5
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 49: ↑47 and ↓2+45
Comments26

Articles