На эту тему было уже много статей, но раскрыта далеко не вся правда. Для тех, кто пропустил — читайте Создание приложений реального времени с помощью Server-Sent Events .
Простой пример:
Браузер устанавливает 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.
Зависимостей от библиотек — нет, все что нужно для работы:
Браузеры ограничивают кол-во одновременных соединений, что при открытии сайта в нескольких вкладках браузера может вызывать проблемы. Конечно, в современных браузеров число соединений ограничено минимум шестью, но если каждая вкладка с вашим сайтом использует 2 или более длинных http-соединения, то этот предел быстро достигается.
Для решения этой проблемы как нельзя лучше подходит SharedWorker, создав SharedWorker мы можем создать внутри него EventSource и рассылать все события с объекта EventSource всем подключившимся скриптам:
Пример, sharedworker.js:
Но, к сожалению, 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).
Когда Соединение с сервером разрывается (либо сервер закрыл соединение, либо произошла какая-то ошибка сети), 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
Как же работает 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