Технология Comet для построения быстрых веб-приложений. Статья в журнал Хакер
Статья написана специально для журнала Хакер и опубликована на сайте журнала (в несколько сокращенной и отредактированной версии). Ниже я публикую свой оригинальный вариант, без правок или ограничения на объем материала.
Comet… чё за …?
Сегодня все в Интернете хочет быть быстрым, очень быстрыми. Ты даже не успел загрузить страничку в Facebook или вКонтакте, а твои друзья уже узнают, что ты онлайн и пытаются вызвать в чат. На других сайтах ты можешь видеть уже не просто ники пользователей, а сразу начать с ними общаться – и это не просто личные сообщения, как было в форумах в лихих 90-х, а полноценные чаты типа аське. Добавил Вася новую статью в блог, ленту новостей которого ты читаешь? Она должна сразу появиться у тебя в ридере, если, конечно, ты сейчас его читаешь. Задержка, любое промедление сразу ставит крест на любом ресурсе, нацеленном на общение. Посмотри на всякие конференции стартапов и первые страницы популярных ресурсов - везде умные слова вроде real-time, collaborative и communication. Что за …?
Отвечаю! Если раньше веб-приложения были, как и обычные сайты – просто страницы и ты должен был ходить по ссылкам, чтобы обновить информацию, разве что по таймеру обновлялись фреймы, то теперь все иначе. Все хотят, чтобы информация приходила к нам сама в момент появления, а не ждать и тыкать сто раз в рефреш, ожидая, когда же твой друг наконец появиться в сети. Такие требования поставили перед разработчиками новые задачи, существенно усложнив жизнь, но, а что делать? Так и родилась технология Comet или, по простому, набор приёмов и средств для обновления данных (страниц или их элементов) в браузере без твоего участия. От тебя надо только залогиниться и зайти на сайт – в социальную сеть, онлайн игру или другой ресурс. От этого момента, если какая-то новая информация появляется, сервер сам отправит твоему браузеру новые данные.
В этом и отличие от AJAX-а, который запускается только если надо отправить данные, и завершает свою нелегкую трудовую жизнь после получения ответа. К сожалению, создание запроса через AJAX это всегда дорого и долго, да и браузер выкидывает свои штучки, ограничивая количество одновременных соединений. Но зато AJAX это самое родное для веба – он работает точно так же как и HTTP-протокол, по схеме запрос-ответ, а вот Comet, чтобы раскрыть свою мощь, должен прибегать к различным извращениям, имитируя постоянное соединение.
Самым первым вариантом применения такого подхода стали чаты. Удобно – новые сообщения появляются мгновенно, страница не обновляется – полное впечатление обычного приложения. Конечно, во Flash уже давно такое можно сделать, но черт бы побрал этот флеш – он тормозит, глючит и закрытый. Да и если ты запрограммировал свое супер-творение на модной технологии AJAX, то включать туда совершенно инородное тело Flash, с которым работать, мороки не оберёшься, никак не хочется. А Comet позволяет обойтись обычными простыми средствами – JavaScript + HTML, получая при этом мгновенность и простоту.
Думаешь, это годится только для чатов и соц.сетей? Никак нет, технологию сразу взяли на своё воображения такие крупные монстры, как банки, биржи и разные финансовые организации. Видел в кино бегущую строку новостей и прыгающие туда-сюда цифры стоимость валют? Теперь такое можно сделать не только в специальных и безумно дорогих программах за миллионы долларов, но и на обычной веб-странице, которую отроешь любым браузером. Так что Comet – это не игрушка, а серьёзный инструмент для всех, кому хочется всего и сразу, и побыстрее!
Существуют коммерческие сервера для COMET-а, которые как раз применяются на биржах и прочих сайтах, но и стоят они соответственно, например, Lightstreamer, от 50 тысяч буржуйских денег. Хотя используют те же самые техники, что и я тебе опишу.
Разбираем по кирпичику.
Давай попробуем разобраться, как устроена технология и как вообще можно сделать так, что информация мгновенно появляется у тебя в браузере. Ты, наверное, знаешь, что основным методом работы с сервером в веб-приложении, является объект XMLHTTPRequest, который позволяет JavaScript-y соединяться с сервером, передавать и получать информацию. Но это, как и обычный HTTP-протокол – ты запрашиваешь URL и получить ответ, не более. Поэтому, чтобы сделать обновляемый список контактов со статусами он/оффлайн через AJAX, необходимо по таймеру, раз в 10 секунд, постоянно соединяться с сервером и запрашивать, кто онлайн. Это существенная нагрузка на браузер, не говоря уже о сервере (хоть он и мощный, но если у тебя на сайте тысяча человек сразу, любой сервер сляжет от стольких запросов). Чем больше интервал опроса, нагрузка будет уменьшаться, но сегодня уже никому не надо, чтобы, когда твой друг уже пиво пьет во дворе, ты несколько минут все ещё видишь его статус «онлайн». Непорядок. Не подходит обычный AJAX для такого.
У COMET-а же соединение обычно одно, но длительное. Классическая схема (называется Long-polling, длинный опрос) выглядит так – ты подключаешься к серверу и ждёшь, пока не появятся данные. Так можно ждать часами, хотя обычно время меньше, максимум 5 минут, после которых просто создаётся новое соединение. Все же сервера не особо любят такие долгоживущие сессии, да и сам HTTP не очень приспособлен к такому использованию, хоть это один из простейших методов Comet-а. Обычный Apache умрёт уже после первой сотни пользователей, поэтому не особо рассчитывай на него, уж лучше сразу ставить Nginx.
Комета изнутри.
Спорим, ты за 5 минут напишешь первое Comet-приложение? Вместе с сервером и клиентом сразу! Заодно на практике рассмотрим, как это работает.
Простейший вариант – скрытый бесконечный фрейм (Hidden iFrame). Это самая старая реализация и самая «хакерская», ведь никто при создании HTML-а не думал, что кому-то необходимо получать данные мгновенно. Работает он так – браузер создаёт невидимый тег <iframe>, указывая ему адрес твоего комет-сервера. Сервер при поступлении запроса отвечает кусочками HTML-кода, например, посылая код javascript, который будет выполнен сразу, как его получит браузер. Фишка в том, что послав первый раз, сервер не закрывает соединение, а ждёт какое-то время, а потом снова и снова отсылает новый скрипт. Браузер-то не резиновый, после некоторого времени в нем накапливается куча элементов <script>, поэтому фрейм необходимо периодически удалять. На сервере также не все гладко, там же настоящий бесконечный цикл. Даже если никаких полезных данных не поступало, сервер все равно должен периодически посылать что-то вроде пинга, говоря клиенту «Я сервер, все ОК». Если же связь оборвётся, узнать это будет затруднительно, поэтому делают так – сервер гарантированно каждую секунду посылает тег <script> в котором вызывает функцию, сбрасывающую предыдущий таймер и запускающую новый. Таймер настроен таким образом, чтобы сработать через, допустим, 5-ть секунд. Если от сервера нет ничего за это время, таймер сработает и удалит фрейм, пробуя заново установить соединение. Если же сервер нормально отвечает каждую секунду, то таймер никогда не выполнится, а значит с соединением все хорошо.
Вот пример такого клиента (с использованием 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();
-
}
Сервер, на РНР:
-
$timeout = 1000000;
-
$running = true;
-
while($running)
-
{
-
echo '<script>comet_new_message('.$msg.');</script>';
-
}
Могут быть сложности с различными прокси-серверами и буферизацией – так как порции данных обычно очень маленькие, некоторые прокси или браузеры будут ждать, пока данных не накопиться несколько Кб, а только потом все передают скопом, что никак нам не подходит. Обычно дело решается добавлением пробелов перед или после сообщения, но траффик возрастает, а gzip сжатие в таком Comet-е невозможно.
Вторая сложность – это нагрузка на сервере, постоянные опросы базы данных или кеша, а также сложность масштабирования. Неприятно и то, что тогда в браузере будет постоянно висеть индикатор загрузки страницы, пока открыт такой IFrame.
Зато этот способ очень быстрый и зависит, в основном, от работы сервера и с какой частотой там цикл опроса.
Сделать механизм передачи по методу Long-polling ещё проще – клиентская часть на JavaScript вообще может быть самой простой, например, в jQuery это всего одна строка: $.getJson('http://comet.domain.com/comet.php', function(response){}); Сервер немного сложнее, чем в предыдущем варианте – вместо бесконечного цикла мы ожидаем первого сообщения, а после его отправки сразу закрываем соединение, завершая скрипт. Заметь, что возвращать можно как JSON, так и любой другой тип данных, доступный для обработки через обычный AJAX.
Долой велосипеды, даёшь автомобиль!
Немного помучавшись, ты сможешь написать простейшую comet-систему на любом языке и использующую любую технологию передачи данных. Но все эти самоделки никак не впишутся в нормальный сайт, если ты хочешь сделать все зашибись. Да и копаться в дебрях JavaScript и добиваться корректной работы во всех браузерах занятие никак не пацанское. Поэтому давай посмотрим на готовые открытые решения, где разработчики уже написали и сервер и клиент и даже API приделали, чтобы можно было встраивать в любой проект.
Если у тебя крутой java-проект, существует почти классическая реализация Comet-а под названием Cometd, но для многих проектов такое решение будет чересчур мощным, да и на низком уровне все работает через long-polling. Это разработка классных чуваков, которые делают Dojo (очень мощный и крутой JavaScript фреймворк на все случаи жизни). Протокол Bayuex поверх обычного AJAX-а, работает по принципу подписки/публикации. Создаются виртуальные каналы, например user/online, а клиент подписывается (subscribe) на один или несколько таких каналов. Когда появляется новая информация, она публикуется (publish) в канал, а все, кто на него подписан, тут же получают сообщение. Особенность техники в том, что и подписчиком и паблишером может выступать как сервер, так и клиент. В обычных реализациях Comet-а роль паблишера возложена на сервер, а клиенты только пассивно ждут, когда сервер соизволит им что-то переслать.
Немного путаницы вносит то, что сервер и клиентские библиотеки Bayeux-протокола называются Cometd, хотя это всего лишь одна из реализаций технологии Comet вообще. На нижнем уровне хваленный Bayeux все равно передаётся одной из базовых реализаций Comet-а, либо Long-polling, либо, реже, стримингом.
Для очень мощных решений, когда надо ограниченными ресурсами поддерживать десятки тысяч соединений, есть отличный сервер APE push engine (http://www.ape-project.org), написанный на C. (мы уже писали обзор). Он поддерживает все виды реализации Comet-а, от long-polling до новомодного WebSockets. Клиентская библиотека реализована на базе Mootools и очень удобная, есть даже эмуляция TCP-сокетов, серверную часть предлагают расширять модулями на JavaScript.
Если необходимо быстро и просто вписать возможности Comet-а в существующий проект и у тебя Nginx, выручит специальный модуль, HTTP_Push_Module, где все взаимодействие с сервером, получение данных и публикация, происходит посредством обычных HTTP-запросов. Это позволяет очень просто добавить возможность обмена сообщениями в реальном времени между любыми системами и языками, где есть поддержка GET/POST запросов.
Dklab_Realplexor – Наш ответ буржуям!
И самый мощный и интересный проект – Dklab_Realplexor от известного разработчика Дмитрия Котерова, создателя Denwer-а, социальной сети Мой Круг и русского твиттера Рутвит. Этот сервер написан на Perl и, по заявлению автора, готов к промышленной эксплуатации с десятками и сотнями тысяч клиентов одновременно. Пока реализована только модель long-polling, но в отличии от остальных решений, автор позаботился о простых людях, предоставив сразу библиотеки для JavaScript и РНР (вот за это спасибо!). На сервере тебе достаточно только подключить один РНР-файл и создать объект сервера, далее можно публиковать сообщения в каналы. С другой стороны, на веб-странице достаточно подключить только небольшой JS-файл и подписаться на интересующие каналы, определив функцию-калбек, которая выполнится при поступлении данных. Сервер внутри имеет собственную очередь сообщений, поэтому данные клиенту доставляются точно в той последовательности, как были отосланы, даже в случае потерь связи или перехода на другую страницу сайта. Радует также лёгкая возможность одной командой получить список всех пользователей онлайн (тех, кто слушает каналы сервера) или изменения этого списка с момента последнего опроса (если у тебя тысяча пользователей, то лучше всего отслеживать изменения, кто вошёл-вышел, а не получать каждый раз список всех).
Давай на примере этого решения напишем простейшую систему обмена мгновенными сообщениями между пользователями на сайте. Сообщение пользователя отправляется на сервере обычным AJAX, а вот доставкой занимается уже Comet.
Код отправки мы опустим, думаю, ты напишешь его за 5 минут. А что дальше?
РНР-код сервера:
//рассылаем сообщение всем пользователям, кто сейчас на сайте
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’);
WebSockets или хватит BDSM!
HTML 5 намозолил уже всем глаза, обещая просто сказочные возможности, но неизвестно когда и как. Но вот одна штука в нем есть уже сейчас – веб-сокеты. Это немного не те сокеты, о которых принято говорить в традиционных языках программирования, но достаточно близко. По сути, Web Sockets – это расширение стандартного HTTP-протокола, которое позволяет устанавливать двухстороннюю, асинхронную связь между клиентом (браузером) и сервером, без ограничения на тип передаваемых данных.
Начинается все обычным образом – браузер посылает серверу специальный HTTP-GET запрос, а вот дальше… если сервер согласен, то он, ответив браузеру согласием, оставляет TCP-соединение открытым! Так что ты дальше имеешь обычный TCP-сокет с сервером, правда, поверх HTTP-протокола. Если одна из сторон хочет что-то передать, она просто пишет в открытый канал свои данные, кодируя их в обычную строку UTF-8 (отмечая начало и конец порции данных шестнадцатеричными флагами) или посылая напрямую бинарные данные. Теперь не надо никаких AJAX/Comet-извращений!
Работать с веб-советами не сложнее чем с обычным AJAX – указываешь обработчики соединения, приёма данных и все! Время жизни такого соединения также не имеет особых ограничений – хоть сутками без перезагрузки. На серверной стороне также все хорошо – большинство популярных веб-фреймворков и платформ заявили о поддержке протокола веб-сокетов в своих продуктах, а некоторые уже успели и сделать. Плохо только, что браузеры слабо спешат поддерживать эту красотищу – сейчас только супермодный браузер Google Chrome поддерживает эту возможность. Для тех же, кому не терпится попробовать, можно использовать костыльное решение, библиотеку web-socket-js, которая имитирует API, согласно текущему проекту стандарта, а внутри работает через Flash-объект, в котором есть реализация полноценных сокетов, хоть и с многими ограничениями.
Не скрою, веб-сокеты действительно революционная штука и способная поменять весь интернет, переведя его в новую эру. Вот только пока это лишь проект, стандарт HTML 5, частью которого и является Web-sockets, все ещё в стадии рассмотрения и, чем черт не шутит, может поменяться.
Comet-сервер изнутри для любознательных
Если ты хочешь стать крутым веб-разработчиком и писать сервера, работающие с сотнями и тысячами пользователей сразу, тебе пригодиться краткая справка о том, как устроены такие решения изнутри.
Классические веб-серверы, например, Apache (без специальной настройки) работает по принципу запрос -> процесс-обработчик. Для Comet-а это сразу отметаем, так как порождение процесса ОС задача очень ресурсоёмкая. Более продвинутая модель pre-fork также не выход, нам же надо тысячи процессов и сразу.
Второй вариант – на каждый запрос создаётся отдельный поток внутри процесса сервера. Это уже лучше, так как поток гораздо легче создать и их можно сделать очень много. Но здесь уже начинаются сложности для программиста, так как работать с множеством потоков – это надо быть крутым хакером и вообще, мыслить как компьютер! Для облегчения этого дела есть множество фреймворков для разных языков.
Эффективные Comet-сервера работают на основе асинхронной модели, используя один, главный процесс сервера и обрабатывая все соединения без переключения между потоками. В этом помогает и сама ОС, предоставляя библиотеки для асинхронного ввода/вывода, например, Libevent. Такая архитектура уже может обрабатывать тысячи одновременных клиентов, а если необходимо масштабирование на несколько процессоров или в кластере, то используется запуск нескольких копий сервера. Так работают сервера на базе Node.js (ага, это серверный JavaScript на движке V8 от Google Chrome) и Twisted/Tornado на Python-е.
И на конец, самые современные веянья в области многопоточных и масштабируемых систем: микронити в языке Erlang, для которого есть и хороший HTTP-фреймворк, MochiWeb (по сути, это реализация собственного механизма потоков средствами языка и среды выполнения, без прямой работы с родными потоками ОС). Аналогичные штуки есть и в Python (Stackless Python), на базе Java VM реализаций просто куча, включая специальные языки вроде Akka (а это реально крутая штука, изучи на досуге - http://akkasource.org)
Уфф, здесь и сказке конец.
Как видишь, все очень и очень просто. И, главное, очень быстро! Поверь, реализация чата на базе Dklab_Realplexor работает с той же скоростью, а иногда и быстрее, чем чат в Facebook-e (автор не гонит, просто засекай время от получения сервером сообщения и до момента вызова callback-функции на клиенте, реально это сотые доли секунды, в зависимости от скорости интернет-соединения).
Comet позволяет создать постоянное соединение между страницой в браузере и веб-сервером и напрямую обмениваться информацией, при этом сервер сам решает, когда послать данные, а страница получает их с той же скоростью, как возникают события. Если это чат, что как только сообщение будет сохранено в базе данных, его сразу же получат все адресаты. Если использовать классический AJAX, что тебе придётся по таймеру спрашивать сервер, «а есть что для меня?». Ты написал сообщение, и теперь ожидаешь, когда же страницы твоих друзей снова спросят сервер о новых данных. Это долго и чат из мгновенного превращается в форум.
С приходом HTML5 в браузерах будет нативное решение – бинарный протокол, упакованный в стандартный HTTP, который обеспечит максимальную скорость обмена данными, что важно, например, для интернет-биржи или аукциона, где новые ставки могут появляться десятками в секунду. Но черт побери, когда же это будет! А пока – Comet это единственный выход, если ты строишь веб-приложение реального времени.
Streaming vs. Polling! Кто кого и где?
Существуют два основных подхода к COMET-у. Long-polling предполагает, что твой скрипт обращается к серверу и говорит – “когда у тебя будут данные, я их сразу заберу, а потом снова подключусь”. В некоторых реализациях сервера существует буферизация, когда сервер не сразу отдаёт данные, а ждёт, вдруг сейчас ещё будут, но обычно она вредна, так как вносит задержки, а мы же хотим максимально быстро! После получения данных браузер должен снова открыть новое соединение.
Стриминг (Streaming) означает, что соединение не обрывается после каждого сообщения, а остаётся открытым, поэтому никаких задержек нет, и единожды соединившись, клиент начинает получать все новые сообщения. Конечно, периодически приходится переподключаться, чтобы очистить браузер от устаревшей информации, да и серверу дать передохнуть, но время соединения здесь уже часы и более против секунд, максимум минут в случае Long-polling-а. Он уже, обычно, требует специального сервера и непростого программирования. Хотя клиентская часть, в браузере, может быть очень даже простой. Сложность, как всегда, в мелочах – например, как определить, что соединение не оборвалось, а просто может у сервера нет для тебя данных? А как масштабировать свою систему? Если обычные HTTP-запросы можно раскидывать на несколько сервером через балансировщик, то с такими долгоживущими соединениями дело значительно хуже. В общем, вопросов и проблем много, а выигрыш не всегда однозначен.
Что же лучше? Смотри сам – если события, о которых тебе надо оповещать пользователей происходят достаточно редко, например, вход/выход на сайте, сообщения чата или новости, то Long-polling позволит сделать все быстро и легко, с минимальными сложностями. Даже обычный слабенький сервер сможет обслуживать тысячи таких пользователей одновременно, а чтобы запрограмить такое на javascript потребуется всего-ничего, несколько строчек да любая из AJAX-библиотек. Если же таких событий происходит очень много, и время между двумя событиями очень мало и меньше, чем тратит клиент на то, чтобы вновь присоединиться к серверу, значит тебе необходим стриминг. Иначе, получив первое сообщение, браузер клиента отключиться, тем временем сервер уже приготовит ему новую порцию данных, и как только клиент появиться, заставит его подключаться снова и снова. А это очень негативно сказывается на быстродействии браузера. Стриминг же один раз подключившись, сможет обработать столько информации, сколько ты ему пошлёшь, и все в рамках одного подключения. Именно поэтому все COMET-сервера для биржевых порталов работают именно на базе стриминга – ведь куры валют за время, пока браузер соединиться с сервером, могут поменяться уже несколько десятков раз!
Jabber в вебе
Известный всем Jabber (а правильно – XMPP) имеет возможность работать и через обычный веб. Эта штука называется BOSH - Bidirectional-streams Over Synchronous HTTP. Его часто используют, если от Comet-а требуется только организация общения типа чата – кто, как не Jabber идеальнее подходит для «болталки», тем более легко можно организовать общение между любыми пользователями, хоть со страницы сайта, хоть с любого Jabber-клиента. И с сервером проблем нет никаких, Jabber есть везде и очень хорошо держит нагрузку, масштабируется. Не веришь? Скажу лишь одно – чат в сети facebook работает именно на базе Jabber-а (внутри, то, что на сайте – уже привычный тебе long-polling).
Примечание:
Всегда создавай для comet-а отдельный поддомен, например, comet.domain.com, так как браузеры не могут создавать параллельно более 2 – 6 соединений с одним доменом, а если на странице с десяток картинок, скриптов и стилей, все соединения будут исчерпаны. Поддомен же считается отдельно, и скрипт сможет работать с ним параллельно.
Ссылки:
Интересен проект jWebSocket – Java-платформа для работы с Web Sockets. Сайт проекта: http://jwebsocket.org/
На русском о Web Sockets: http://habrahabr.ru/blogs/webdev/79038/ и http://soullink.habrahabr.ru/blog/82140/
Хорошая вводная статья о AJAX-е: http://javascript.ru/ajax/intro
Для jQuery можно использовать плагин - JQuery PeriodicalUpdater
Ещё Comet-а:
Atmosphere (Java фреймворк) https://atmosphere.dev.java.net
WebSync (.NET) - http://www.frozenmountain.com
Orbited (Python) - http://orbited.org
Juggernaut (Ruby) - http://juggernaut.rubyforge.org
PHPDaemon (PHP) - http://github.com/kakserpom/phpdaemon
Faye (Node.JS) - http://github.com/jcoglan/faye
Свежие комментарии