Содержание статьи
Когда-то веб состоял только из статических страничек. Позже появились динамический контент и технология AJAX, позволяющая частично обновлять содержание страниц. Но для реализации приложений, работающих в реальном времени — онлайн игр, биржевых программ, отдельных частей социальных сетей, — этого оказалось мало. На свет появилась технология Comet.
Веб-приложения, как и обычные сайты, начинались как статические HTML-ки с формой для ввода некоторого запроса, при этом пользователь сам должен был переходить по ссылкам, чтобы обновить информацию.
Теперь все иначе. Каждый хочет, чтобы информация приходила к нему сама, причем в самый момент ее появления. Ждать или того хуже нажимать на рефреш в ожидании обновления уже неприемлемо. Если пришло сообщение в социальной сети, этот факт тут же должен отобразиться в открытой странице браузера. Помимо этого, начался поголовный перенос софта в веб-окружение, предъявляя к веб-приложениям требования обычных десктопных программ. Для веб-приложения, работающего с финансовой биржей, задержки неприемлемы, и, чтобы обойти массу ограничений, используется технология Comet.
Это не AJAX
Это не чистящий гель, но он также может навести блеск, правда, не на кухне, а в вебе. Если кратко, то Comet — это набор приемов и средств, позволяющих быстро обновлять данные в браузере (страницы и их элементы) без участия пользователя. От клиента требуется только зайти на ресурс — с этого момента сервер сам будет отправлять браузеру новые данные, если для него есть какая-то новая информация. Сразу хочу обратить внимание на ключевое отличие от AJAX, который запускается только в том случае, если надо отправить данные, и завершает свою нелегкую трудовую жизнь сразу после получения ответа.
К сожалению, создание запроса через AJAX — это всегда дорого и долго, да и браузер выкидывает свои штучки, ограничивая количество одновременных соединений. Правда, AJAX — это самое родное для веба, потому как работает он точно так же, как и HTTPпротокол, по схеме запрос-ответ. Совсем другое дело — Comet, которому для реализации всех своих возможностей, приходится прибегать к различным извращениям, имитируя постоянное соединение.
Ключевой момент Comet'а заключается в том, что не клиент (браузер), а сервер решает, когда надо отправить пользователю данные — такой подход называется serverpush. Самым первым вариантом применения такого подхода стали чаты: новые сообщения появляются мгновенно, страница не обновляется — полное впечатление обычного приложения. Конечно, аналогичную функциональность давно можно реализовать через Flash, но черт бы побрал этот флеш — он тормозной, глючный и закрытый. Да и если ты запрограммировал свое супер-творение на технологии AJAX, то включать туда совершенно инородное тело Flash, работать с которым мороки не оберешься, никак не хочется. А Comet позволяет обойтись обычными простыми средствами — JavaScript + HTML, получая при этом мгновенность обновлений и простоту.
Разбираем по кирпичику
Так как же удается Comet’у мгновенно обновлять информацию у тебя в браузере?
Ты, наверное, знаешь, что основным методом работы с сервером в веб-приложении является объект XMLHttpRequest, который позволяет JavaScript’у соединяться с сервером, передавать и получать информацию. Принцип действия аналогичен классическому HTTP-протоколу: ты запрашиваешь URL и получаешь ответ. Поэтому, чтобы через AJAX сделать обновляемый список контактов со статусами (онлайн/офлайн), необходимо по таймеру, раз в 10 секунд, регулярно соединяться с сервером и запрашивать список тех, кто онлайн. Это существенная нагрузка на клиентский браузер, не говоря уже о самом сервере: каким бы мощным он ни был, выдержать нагрузку тысяч людей, каждый из которых будет отправлять такое количество запросов, скорее всего ему не удастся. Да, чем больше интервал опроса, тем меньше нагрузка. Но сегодня никто не хочет мириться с задержками: мало кому приятно отправлять сообщения пользователю со статусом «Онлайн», когда он давно убежал на речку, а статус банально не успел обновиться. Короче говоря, обычный и так привычный нам AJAX тут уже не подходит.
Существуют два основных подхода реализации Comet'а. Классическая схема (называется Long-polling, длинный опрос) выглядит так — ты подключаешься к серверу и ждешь, пока не появятся данные. То есть твой скрипт обращается к серверу и говорит: «Когда у тебя будут данные, я их сразу заберу, а потом снова подключусь». В некоторых реализациях сервера существует буферизация, когда сервер не сразу отдает данные, а ждет: вдруг сейчас появится что-то еще, тогда отправлю все сразу. Но такая буферизация вредна, так как вносит задержки, а мы хотим достичь максимальной скорости! После получения данных браузер должен снова открыть новое соединение. Длительность такого соединения может измеряться часами, но это в теории. Обычно время намного меньше и максимум достигает 5 минут, после которых просто создается новое соединение. Делается это потому, что серверы не любят такие долгоживущие сессии, да и сам протокол HTTP не очень приспособлен к такому использованию.
Другой подход называется стримингом (Streaming) и означает, что соединение не обрывается после каждого сообщения, а остается открытым. В результате, единожды подключившись, клиент начинает получать новые и новые сообщения без каких-либо задержек. Конечно, периодически приходится переподключаться, чтобы очистить браузер от устаревшей информации, да и серверу дать передохнуть, но время соединения здесь уже измеряется часами против секунд, максимум минут в случае Long-polling’а. Такой подход значительно сложнее и, как правило, требует специального сервера. Сложность, как всегда, проявляется в мелочах — например, как определить, что соединение не оборвалось, а у сервера просто нет для тебя данных? А как масштабировать свою систему? Если обычные HTTP-запросы можно раскидывать на несколько серверов через балансировщик, то с такими долгоживущими соединениями алгоритмы должны быть значительно сложнее. Словом, у каждого из подходов есть свои плюсы и минусы.
Комета изнутри
Чтобы понять, как устроен Comet, предлагаю написать реал-тайм приложение на практике: сервер и клиент. Простейший вариант реализации — скрытый бесконечный фрейм (hidden iframe). Это самая старая и самая «хакерская» реализация Comet'а, ведь никто при создании HTML-а не думал, что появится необходимость получать данные мгновенно.
Работает он так: браузер создает невидимый тег <iframe>, указывая ему адрес твоего комет-сервера. Сервер при поступлении запроса отвечает кусочками HTML-кода: например, посылает код JavaScript, который выполняется сразу, как только его получает браузер. Фишка в том, что отправив данные в первый раз, сервер не закрывает соединение, а ждет какое-то время, а потом снова и снова отсылает новый скрипт. В итоге данные постоянно обновляются, но тут проблема: браузер-то не резиновый. Через некоторое время в нем накапливается куча элементов <script>, поэтому фрейм необходимо периодически удалять. На сервере также не все гладко, потому как на нем крутится настоящий бесконечный цикл.
Даже если никаких полезных данных не поступает, сервер все равно должен периодически посылать что-то вроде пинга, говоря клиенту: «Я сервер, все ОК». Если же связь оборвется, узнать это будет затруднительно, поэтому сервер гарантированно каждую секунду посылает тег <script>, в котором вызывает функцию, сбрасывающую предыдущий таймер и запускающую новый отсчет. Таймер настроен таким образом, чтобы сработать через 5 секунд (значение может сильно варьироваться). Если от сервера за это время ничего нет, таймер сработает и удалит фрейм, пробуя заново установить соединение. Если же сервер нормально отвечает каждую секунду, то таймер никогда не выполнится, а, значит, с соединением все хорошо. Вот пример такого клиента (с использованием JS-библиотеки jQuery):
var error_timer_id = null;
function error_iframe()
{
$('#comet_iframe_panel').
empty().append('<iframe
src="comet.domain.com/comet.
php?user_id=1"></iframe>');
}
function comet_ping()
{
clearInterval(error_timer_id);
setInterval(function(){ error_
iframe(); }, 5000);
}
function comet_new_message(msg)
{
$('#comet_msg_content').
append('<div>' + msg.time + ': '
+ msg.text + '</div>');
comet_ping();
}
Сервер, реализующий бесконечный цикл, при этом может выглядеть следующим образом (в реализации на PHP):
$timeout = 1000;
$running = true;
while($running)
{
$msg = '{time:'.date('m:s',
time()).',text:”Server says:
OK!”}’';
echo '<script>comet_new_
message(’.$msg.’);</script>’;
usleep($timeout);
}
Это одна из самых простых реализаций Comet'а, но от этого и обладающая рядом недостатков. Иногда бывают проблемы с различными прокси-серверами и буферизацией. Так как порции данных обычно очень маленькие, некоторые прокси или браузеры будут ждать, пока не накопится нужное количество данных, и лишь потом будут передавать все скопом. Нам это не подходит.
Обычно дело решается добавлением пробелов до или после сообщения, но трафик при этом неизбежно возрастает, а gzip-сжатие в такой реализации Comet’а невозможно. Вторая сложность — это нагрузка на сервер, постоянные опросы базы данных или кэша, а также сложность масштабирования.
Неприятно и то, что в браузере постоянно будет висеть индикатор загрузки страницы, пока открыт такой iframe. Зато этот способ очень быстрый и зависит в основном от работы сервера и того, с какой частотой осуществляется на нем цикл опроса. Сделать механизм передачи по методу Longpolling еще проще — клиентская часть на JavaScript вообще может быть миниатюрной: например, в jQuery это всего одна строка: $.getJson(‘http://comet.domain.com/comet.php’, function(response){});
.
Сервер немного сложнее, чем в предыдущем варианте: вместо бесконечного цикла мы ожидаем первого сообщения, а после отправки ответа сразу закрываем соединение, завершая скрипт. Заметь, что возвращать можно как JSON, так и любой другой тип данных, доступный для обработки через обычный AJAX.
Долой велосипеды, даешь автомобиль!
Немного помучившись, ты сможешь написать простейшую Сomet-систему на любом языке, использующую любую технологию передачи данных. Но все эти самоделки едва ли впишутся в нормальный сайт. Копаться в дебрях JavaScript и добиваться корректной работы во всех браузерах — занятие весьма муторное. Но! Есть готовые к внедрению решения. Я не буду говорить про коммерческие серверы для Comet'а: они, как правило, применяются в биржевом софте и стоят соответствующих денег (например, стоимость Lightstreamer начинается от 50 тысяч долларов).
Наряду с платными решениями широкое распространение получили готовые открытые проекты, в рамках которых разработчики уже написали серверные и клиентские части, а также приделали к ним API, чтобы можно было легко встраивать в любой проект. Это Cometd (cometd.org), HTTP_Push_Module (pushmodule.slact.net), APE push engine (www.ape-project.org). Я же хочу рассказать тебе о проекте от известного разработчика Дмитрия Котерова, создателя Denwer’а, социальной сети Мой Круг и русского твиттера Рутвит.
Сервер Dklab_Realplexor написан на Perl и, по заявлению автора, готов к промышленной эксплуатации с десятками и сотнями тысяч клиентов одновременно. Пока реализована только модель long-polling, но в отличие от многих других решений, автор позаботился о простых людях, предоставив сразу библиотеки для JavaScript и РНР. На сервере тебе достаточно только подключить один РНР-файл и создать объект сервера, далее можно публиковать сообщения в каналы, чтобы передавать клиентам данные. С другой стороны, на веб-странице достаточно подключить только небольшой JS-файл и подписаться на интересующие каналы, определив функцию callback, которая выполняется при поступлении данных.
Сервер внутри имеет собственную очередь сообщений, поэтому данные клиенту доставляются точно в той последовательности, как были отосланы, даже в случае потери связи или перехода на другую страницу сайта. Радует также удобная возможность одной командой получить список всех пользователей онлайн (тех, кто слушает каналы сервера) или изменения этого списка с момента последнего опроса (если у тебя тысяча пользователей, то лучше всего отслеживать изменения, а не получать весь список заново).
Давай на примере этого решения напишем простейшую систему обмена мгновенными сообщениями между пользователями на сайте. Сообщение пользователя отправляется на сервере обычным AJAX'ом, а вот доставкой занимается уже Comet. Код отправки мы опустим — это слишком просто (смотри исходники). Намного интереснее посмотреть на сервер, который мы напишем на PHP:
//рассылаем сообщение всем пользователям, которые сейчас на сайте
include_once(‘Dklab/Realplexor.php’);
//подключаемся к серверу для отправки сообщений
$dklab = new Dklab_Realplexor("127.0.0.1",
"10010", "xakep_");
//10010 — специальный порт для приема сообщений к отправке
//xakep_ — префикс для каналов, чтобы один сервер мог обслуживать разные проекты
$_to = Array(‘all_online’); //массив каналов, куда нужно отправлять сообщение. В этом случае используется общий канал, который слушают все пользователи, которые на сайте
$_message = Array('text' => 'Привет от журнала
Хакер!’, 'author' => ‘Вася', 'time' =>
time());
//это сообщение, которое в виде JSONобъекта будет передано всем пользователям
$dblab->send($_to, $_message);
//сообщение отправлено! Код на JavaScript с клиентской стороны, который принимает сообщения, не сильно сложнее:
//создаем подключение к серверу
var comet = new Dklab_Realplexor('http://
rpl.domain.com', 'xakep_');
//сервер требует обязательной работы на поддомене
//теперь подписываемся на канал, чтобы получать все отправленные в него сообщения
comet.subscribe("all_online", function (msg, id){
//этот метод будет выполнен для каждого полученного сообщения
//id — это внутренний уникальный идентификатор сообщения
$('#comet_msg').append('<div><b>' + new Date(msg.time * 1000).toLocaleString() + '</b> ' + msg.author + ': ' + msg.text + '</div>');
//обрати внимание, что ты на сервере отправил обычный РНР-массив — на странице у тебя JSON точно такой же структуры
});
come.execute(); //после определения подписок приказываем слушать их
//подписываться и отписываться можно в любой момент, необходимо только вызвать comet.execute(), чтобы уведомить сервер
//все, не хотим получать сообщения comet.unsubscribe(‘all_online’);
Здесь и сказке конец
Comet позволяет создать постоянное соединение между страницей в браузере и веб-сервером и напрямую обмениваться информацией. При этом сервер сам решает, когда послать данные, а страница получает их с той же скоростью, как возникают события. Несмотря на всю мощь, использовать Comet очень просто, особенно если взять на вооружение готовые проекты вроде Dklab_Realplexor. При этом решение получается по-настоящему быстрым. Поверь, реализация чата на базе Dklab_Realplexor работает с той же скоростью, а иногда и быстрее, чем чат в Facebook'e. С приходом HTML5 в браузерах будет родное решение — бинарный протокол, упакованный в стандартный HTTP, который обеспечит максимальную скорость обмена данными, что важно, например, для интернет-биржи или аукциона, где новые ставки могут появляться десятками в секунду.
Но когда же это будет? На данный момент Comet — это единственный выход, если ты строишь веб-приложение, работающее в реальном времени.
Info
Всегда создавай для Comet’а отдельный поддомен, например, comet.domain.com, так как браузеры не могут создавать параллельно более 2–6 соединений с одним доменом, а если на странице с десяток картинок, скриптов и стилей, лимит соединений будет исчерпан. Подключения на поддомен считаются отдельно.
Links
- Подробнее о вебсокетах: websockets.ru/tech/intro
- Хорошая вводная статья об AJAX-е: javascript.ru/ajax/intro
Фреймворки для реализации вебприложений в реальном времени:
- Java — atmosphere.dev.java.net;
- .NET — www.frozenmountain.com;
- Python — orbited.org;
- Ruby — juggernaut.rubyforge.org;
- PHP — github.com/kakserpom/phpdaemon.
Да здравствуют Websockets!
Стандарт HTML 5 уже успел намозолить глаза, обещая сказочные возможности, но неизвестно когда и как. Но одна штука в нем есть уже сейчас — веб-сокеты. Это немного не те сокеты, о которых принято говорить в традиционных языках программирования, но они достаточно близки к ним.
По сути, WebSockets — это расширение стандартного HTTP-протокола, которое позволяет устанавливать двухстороннюю асинхронную связь между клиентом (браузером) и сервером без ограничения на тип передаваемых данных.
Начинается все обычным образом: браузер посылает серверу специальный HTTPGET запрос, но дальше… Если сервер согласен установить запрошенный вид подключения, то он отправляет браузеру клиента специальный флаг и оставляет TCP-соединение открытым! В результате имеем обычный TCP-сокет с сервером, правда, поверх HTTP-протокола. Если одна из сторон хочет что-то передать, она просто пишет в открытый канал свои данные, кодируя их в обычную строку UTF-8 (отмечая начало и конец порции данных шестнадцатеричными флагами) или посылая напрямую бинарные данные.
Наконец-то можно будет избавиться от всех ограничений AJAX и извращений Comet’а.
На текущий момент, единственным браузером, поддерживающим веб-сокеты, является Google Chrome. Самые нетерпеливые могут опробовать веб-сокеты в действии, правда, с помощью довольно костыльного решения. Я говорю о библиотеке web-socket-js (github.com/gimite/web-socket-js), которая имитирует API, согласно текущему проекту стандарта, а внутри работает через Flash-объект, в котором есть реализация полноценных сокетов, хотя и с многими ограничениями.
Long-polling vs. Streaming
Что же лучше? Все зависит от ситуации. Если события, о которых тебе надо оповещать пользователей, происходят достаточно редко (например, сообщения о входе/выходе юзера, мессаги чата или новости), то Long-polling позволит сделать все быстро и легко с минимальными сложностями. Даже обычный слабенький сервер сможет обслуживать тысячи таких пользователей одновременно, а чтобы запрограмить такое на JavaScript, потребуется всего ничего — несколько строчек кода и любая из AJAX-библиотек.
Если же таких событий происходит очень много, и время между двумя событиями меньше, чем тратит клиент на то, чтобы вновь присоединиться к серверу, значит, тебе необходим стриминг. Иначе, получив первое сообщение, браузер клиента напрасно отключится, чтобы тут же заново подключиться и забрать новую порцию данных — и так будет повторяться вновь и вновь. Это крайне негативно скажется на быстродействии браузера. С помощью же стриминга, один раз подключившись, можно обработать столько информации, сколько будет на этот момент данных — и все в рамках одного подключения.
Именно поэтому все Comet-серверы для биржевых порталов работают именно на базе стриминга, ведь курсы валют могут поменяться несколько десятков раз за время, пока браузер соединится с сервером!