27 июня 2011 в 09:23

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

На эту тему было уже много статей, но раскрыта далеко не вся правда. Для тех, кто пропустил — читайте Создание приложений реального времени с помощью 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
Дмитрий Богаткин @drdim
карма
9,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (26)

  • +4
    Нет, ну насчет лучшей поддержки чем WebSockets, это вы конечно загнули…
    WebSockets + gimite и будет работать практически везде, здесь и сейчас…
    Ну это ладно, у меня вот такой вопрос к Вам, пользуясь случаем, так сказать…
    Вот основное достоинство что WebSockets, что SSE — это то, что по сети не гоняются безудержно запросы проверяющие есть ли новые данные на серваке или нет, как ранее было с ajax, например… Но есть такая проблема: у меня приложение написано с использованием WebSockets и если пользователь просто закрывает ноутбук и ось засыпает, то сервак по-прежнему думает, что пользователь online. js-событие onlose в данном случае не срабатывает. Вот никак не могу решить эту проблему, без возвращения к методологии ajax с постоянным «пингом». Решается ли эта проблема как-то в SSE?

    ЗЫЖ Вопрос может быть и глупый, но без решения этой проблемы что WebSockets, что SSE в реальных проектах мало чем будут отличаться от того же самого чата на ajax
    • 0
      Если ноут засыпает, разве соединения не рвутся?
      • +1
        нет, не рвутся…
      • +4
        то, что не рвутся, это проблема не именно WebSockets а сокетов в целом. даже если есть подключение, и вдруг обрывается канал — никто не знает, что активного подключения уже нет.

        KhanTengri для такого решения, следует поискать реализацию (или самому написать), которая будет посылать пинг во время «слушанья» канала. всё будет так же как раньше с аякс пингом, только тут будет пинг всего один байт, потому что не надо передавать кучу заголовков что приблизительно от 200байт каждый раз.
        • 0
          А зачем пинг делать аякосвым? Почему бы не сделать что бы клиент периодически слал сигналы «Я на связи!» через тот же самый сокет?
          • +1
            Ну а я что про 1 байт говорил? :)
            • 0
              Последствия беглого прочтения, я почему-то подумал что вы хотите посылать пинги средствами аякса. :)
    • 0
      Возможно проблема в вашей сетевой карте и/или настройках BIOS для нее (тот же Wake-On-LAN например).
    • 0
      Я сделал таймаут на стороне сервера — если данных нет, сервер через какое-то время отдает пустой ответ, после чего если клиент ещё на связи, он подключается заново. Для маленьких нагрузок нормально.

      + можно ещё сделать, если клиент уходит со страницы (что чаще всего бывает) — послать параллельно запрос о закрытии «висячего» соединения.
      • 0
        при таймауте — почти тот же самый аякс-пинг
  • +1
    Всё-таки WebSocket, на мой взгляд, более перспективный и интересный механизм чем SSE.
    Случаем никто не знает продвигается ли исправление уязвимости с прокси-серверами? Т.е. иначе говоря когда примерно стоит ждать возвращения WebSocket в Оперу и ФФ?
    • +2
      В FF6 будет уже. Опера — хз
      proof
      • 0
        Опера поддерживает их уже сейчас. Просто по умолчанию они отключены. В настройках легко включается.
        • +1
          Вы попробуйте это пользователю обьясните…
          И в FF они вроде бы тоже включаются таким же макаром, как и в Opera… да только юзер всего этого делать не будет.
  • +1
    http://dev.w3.org/html5/websockets/ — Editor's Draft 21 June 2011
    когда определятся с реализацией, тогда и ждите. А ежели охото работать на сыром под Opera хоть сейчас: opera:config#UserPrefs|EnableWebSockets
  • 0
    А при какой нагрузке сайт ляжет? Сколько онлайн посетителей?
    • 0
      В лучшем случае перестанет отвечать когда закончатся порты, более вероятно что упремся в процессор, маловероятно, что упремся в канал. При такой реализации: «вычислять 1 раз, использовать Node.js Buffer и слать мультикастом» шанс лучшего случая многократно возрастает.
      Для сферической в вакууме задачи конкретные цифры сказать невозможно :)
  • 0
    Подскажите человеку, не очень хорошо разбирающемуся в ajax-тренде. Если асинхронные сообщения от сервера получать через Server Side Events, то как по феншую отправлять асинхронные сообщения на сервер? XMLHttpRequest()? Или есть какие-то более корректные способы, чтобы не плодить TCP подключений и не ждать ответов, так как они приходят асинхронно по SSE?
    • 0
      К клиенту — SSE, от клиента — XHR (всреднем 2 подключения). Более экономный способ — WebSockets (1 подключение)
  • +1
    На мой личный взгляд Server-Sent Events — это попытка отполировать и узаконить хак под названием Comet. Но это не лучшее решение — потому что оно принципиально не приспособлено для создания интерактивных приложений. Полноценную интерактивность может дать только полностью симметричный асинхронный канал. Поэтому надо менять сам принцип работы. Это реализовано в WebSockets.
    Все проблемы которые решает SSE полностью решены WebSockets`ами, причем более элегантно и эффективно.
    • +1
      Я про этом писал полтора года назад — habrahabr.ru/blogs/webdev/79038/
      • 0
        Полностью согласен, сам использую LongPolling и SSE только от отсутствия нормальной альтернативы, ждем WebSockets. Десктопные приложения на всех серьезных языках имели возможности делать полноценные интерактивные приложения в режиме полной двунаправленной асинхронности в два направления еще 15 лет назад. Конечно все готовые RPC типа DCOM и CORBA, были не оправдано сложными и глючными. Но всегда можно было спуститься к TCP/IP и сделать все просто и лаконично. Сейчас, в век веб-интерфейсов все уже как-то подзабыли про это, мы очень долго уже ждем появления нормальной связи. Главная проблема — это безопасность. Если сайты смогут пользоваться сокетами без дополнительных пермишенов, то сразу появятся ботнеты, превращающие компы большого кол-ва людей в сканировщики сетей, плацдармы для дос-атак, прокси-анонимайзеры для хакеров и кравлеры для сливания контента с порталов с диверсификацией IP-адресов. Без решения проблем безопасности нормальная связь не появится. Кто-то в курсе, есть ли уже подходы к решению этой проблемы?
        • 0
          Подходы есть.
          Последния изменения в Websockets во многом направлены на улучшение безопасности. Но даже в самых первых ревизиях уже было заложено, что коннектиться можно не абы куда, только туда, куда разрешит сервер. Поначалу кажется, что это ухудшает безопасность по сравнению в моделью «same origin», использованной в XHR. Но реально, это не добавляет новых дыр, но дает новые возможности. Хотите сделать DDoS-aтаку, пожалуйста — вставьте в html страницы картинку с нужного хоста. Хотите просканировать порты — укажите их и сканируйте, кто мешает. В частности, один гоморесурс уже использовал это для ддос-аттаки.
  • 0
    (немного оффтоп, но тем кому интересен субж может пригодится)
    Недавно мне скинули ссылку на очень интересную либу — Now.JS

    Выглядит очень симпотяшно. Использует socket.io на сервере.
    • 0
      Похожий принцип был у XAJAX (PHP-JavaScript мост), но библиотека как-то не прижилась. Создание прозрачной границы между клиентом и сервером это интересное решение. PHP-JavaScript — не очень оправдано, а вот JavaScript-JavaScript интересный вариант, тем более, что Now.JS выглядит привлекательнее чем XAJAX ;)
  • 0
    WebSockets конечно хорошо, но надо опираться на то, что сейчас есть. Как только обкатают, и решат все проблемы с безопасностью, тогда и будет этот хороший инструмент, сейчас же для задач (согласен, не всех) подойдет и SSE.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.