Используем Nginx как “long polling” (Comet) сервер

Принцип “Long Polling” (или “Comet“) позволяет сделать возможным установление постоянного соединения клиента с Web приложением и также периодичной отправки данных клиенту в рамках этого соединения (без необходимости клиента постоянно делать HTTP запросы для проверки новых данных). Другими словами, эта модель позволяет реализовать постоянные HTTP соединения для получения данных порциями. Наиболее популярное применение - чаты, твиттеры и другие live-updated потоки - клиент постоянно “слушает” сервер на предмет появления новых сообщений, и как только новые сообщения появляются - они мгновенно доставляются ему.
Сам принцип не несет в себе инноваций в HTTP протоколе, т.к. этот подход реализуем обычными средствами. Проблема заключается в том, что стандартное решение - установка постоянного соединения с обычным Web сервером (а значит и с обслуживающей платформой, например php-бекендом) - крайне ресурсоемкое и не применимо на практике. Другой подход - постоянно опрашивать приложение на предмет появления новых данных является еще более ресурсоемким.
Решением этой проблеммы стали т.н. HTTP-PUSH (или comet) сервера, позволяющие облуживать огромное количество постоянных соединений эффективно расходуя ресурсы. Как они устроены и как применяются на практике?
Принцип работы Comet-сервера
Необходимо отметить, что Comet сервер решает задачу отправки клиенту определенных данных порциями. Comet сервера реализуют т.н. HTTP Push Relay протокол (например, Basic HTTP Push Relay Protocol). Принцип работы такого протокола заключается в следующем:
- На Web сервере создается т.н. “канал” с уникальным ключем. Это делается на стороне приложения путем посылки запроса к Comet-серверу
- Клиент устанавливает обычное HTTP соединение с Comet-сервером, передавая в параметры ключ канала, из которого он будет получать данные. Сервер удерживает такое соединение, пока не отправит очередную порцию данных
- Основное приложение, по определенному событию (например, кто-то написал сообщение клиенту), шлет в канал Comet сервера с нужным ключем это сообщение
- Как только в канале появляется сообщение, Comet сервер отправляет его соответствующему клиенту и закрывает HTTP соединение (иногда соединения не закрываются, а продолжают быть активными)
Таким образом, сервер реализует “стэки” сообещений, а их раздача происходит без участия тяжелых бекендов. Бекенды вступают в силу только тогда, когда необходимо отправить сообщение.
Nginx и модуль HTTP Push
Модуль nginx_http_push_module позволяет превратить Nginx в Comet сервер, реализуя протокол Basic HTTP Push Relay Protocol. Преимущества этого модуля и протокола - в его простоте по сравнению с альтернативными решениями (например, протоколом Bayeux и сервером CometD).
Установка
Для установки модуля необходимо скачать его исходники и перекомпилировать Nginx, т.к. модуль является внешней разработкой:
wget http://pushmodule.slact.net/downloads/nginx_http_push_module-0.692.tar.gz tar -xvf nginx_http_push_module-0.692.tar.gz ... cd /where/nginx/sources/are ./configure \ --add-module=/path/to/nginx/modules/sources/nginx_http_push_module-0.692/ make; make install
Конфигурация
Для базовой конфигурации нам необходимо объявить две рабочих точки в Nginx’e: точка публикации сообщений (приватная - доступна только для самого приложения) и точка раздачи сообщений (публичная - к которой будут подключаться клиенты):
location /publish {
# Название переменной с идентификатором канала
# в нашем примере "cid", т.е. запрос будет таким:
# http://example.com/publish?cid=s42378fwe
set $push_channel_id $arg_cid;
push_publisher;
# Отключаем хранение очереди (сообщение удаляется после доставки)
push_store_messages off;
}
location /listen {
push_subscriber;
# Обслуживать только первого "слушателя"
# Остальным отправляем 403
push_subscriber_concurrency first;
# Идентификатор канала
set $push_channel_id $arg_cid;
# Тип ответа
default_type text/plain;
}
После этого при отправке сообщения нужно будет посылать его в соотв. канал, делая POST запрос на “/publish”. Клиент же отправляет GET запрос на “/listen” и ждет ответа. Когда приходит ответ, клиент делает повторный запрос на “/listen” и т.д.
Все параметры конфигурации с подробным описанием смотрите на официальном сайте.
Пример
В качестве примера ниже приведен код на PHP, который используется для отправки сообщения в канал:
# Определяем ID канала $channel_id = 12345; # Сообщение $message = 'Привет тебе!'; # Отправляем сообщение в канал $c = curl_init( 'http://localhost/publish?cid=' . $channel_id ); curl_setopt($c, CURLOPT_RETURNTRANSFER, true); curl_setopt($c, CURLOPT_POST, true); curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($message)); $r = curl_exec($c);
Для получения сообщения код на Javascript будет выглядеть где-то так (на основе Jquery):
var channelId = 12345;
function check_messages() {
$.get('/listen?cid=' + channelId, {}, function(r) {
// Считаем, что у нас есть div c id=messages,
// куда мы дописываем сообщения
$('#messages').append(r);
setTimeout(check_messages, 500);
}, 'json');
}
check_messages();
В каких случаях Вам приходилось пользоваться comet-серверами?


Здорово!
А на connect.ua это используется в чате?
На коннекте очень похожая система, но собственной разработки.
Парни, все это понятно. Один отправляет, второй слушает. Но где вся информация храниться то? Т.е. весь этот поток кто хранить будет? Просто существуют довольно не тривиальные задачи не похожие на простой чат, и как работать с энжинксом в таком случае?
Александр,
Сообщения храняться прямо внутри Web сервера. Конечно, long-polling призваны решать только определенный класс задач, а остальные задачи рушать нужно другим способом. Буду благодарен, если приведете пример задачи, с выбором решения которой затрудняетесь. Спасибо!
Да я пока сам не знаю что хочу, только недавно окунулся в эту тему, поэтому и задаю глупые вопросы. Но больше склоняюсь к освоению node.js в связке с какой то БД. Вам спасибо за ответ, периодически забегаю на ресурс, много полезного.
а как можно месаги обратно на сервер челе “слушателяи” посылать???
Если я правильно понял вопрос, то Вас интересует как посылать сообщения. Смотрите первый пример кода (PHP) - сообщения отправляются через обычный бекенд.
именно, только для отправки сообщения всё равно надо устанавливать соединеия - и отсылать месагу.
через уже открытое соединение можно что-то отправить или только слушать можно???
Конечно для отправки нужно делать отдельный запрос. Механизм long-polling реализуется сверху HTTP, а это значит, что у Вас стандартная последовательность: запрос, потом ответ.
Хочу реализовать task manager удаленной системы. Т.е. писать в канал должен не пользователь “публикатор”, а процесс, который крутиться демоном, делает каждую секунду ps, парстит ответ оси и публикует в канал. Как это сделать?
Что именно Вас интересует, не совсем понимаю для каких целей Вы собираетесь результат ps передавать в канал? Кстати, процесс не обязательно делать демоном - можно использовать крон.
Пример задачи простой и известный: подсказка в поле запроса (suggest). Используется большинством поисковиков сейчас. При наборе каждой буквы нужно проверить какой список слов показать. Одна буква часто меняет список полностью. Естественно, стандартными средствами это решается, но можно ли решить с long polling ?
В этом случае нет необходимости держать длинные соединения, т.к. ответ генерируется сразу без задержек и ожидания. Кроме этого, отсутсвует необходимость периодически опрашивать сервер на предмет обновления данных. Т.е. long-polling для этой задачи использовать нет смысла.
@Den Golotyuk
Понял, спасибо
Спасибо за статью. А позволяет ли nginx_http_push_module использовать не long-polling а стриминг? Просто для написания, например, чата, стриминг был бы предпочтительнее, как я понимаю.
setTimeout(check_messages, 500); - это рекурсия, ведь так? следовательно постоянный опрос сервера, что противоречит вашим же словам - “Другой подход - постоянно опрашивать приложение на предмет появления новых данных является еще более ресурсоемким.” Или я что-то не так понял?
@Павел
Я не видел упоминаний о поддержке стриминга, но, тем не менее, посмотрите подробнее.
@juise
Да, это циклический опрос сервера, но происходит он только после получения ответа от сервера. А суть long-polling - это “удержание” соединения до прихода новых данных. Т.е. в результате - это не “постоянный опрос”, а опрос после ответа.
Кстати а listen оказывается в результате не защищенным? Т.е. средствами Web приложения выходит нельзя ограничить доступ к каналу?
Нет, нельзя. Но подбор ключа не опасен - точнее зависет от сложности ключа - поэтому следует использовать достаточно длинные хеши.
Добрый день. А как реализуеться удержание канала сервером ? Веб сервер получает запрос, в течение времени timeout он должен дать ответ, если время вышло то клиент снова шлет зарос . я правильно понял ?
Спасибо.
@Антон
Нет, сервер удерживает соединение, пока не получит сообщение для отправки “слушателю”.
Node.js теперь может работать с MySQL сервером так что дерзайте хотя. Надо бы протестировать это)
Интересно, как в таком случае решается вопрос авторизации? Например, в случае того-же fb чата. Не создавать же очередь под каждую авторизованную сессию? (Их потом мониторить, прибивать дохлые нужно, плюс сессионный ключ передавать, кхм, не CSRF-но…)
Если же, предположим, сообщения для каждого юзера ложатся в персистентный канал sha1(md5(USER_NAME) . SALT), то такое при наличии N акков, можно забрутить…
Что думаете?
Мы для каждого канала генерируем хеш ключ (довольно длинный, подбор тут неэффективен). Каждый канал имеет свой expiration, т.ч. следить за старыми каналами нет необходимости.
Рядом с хешем пароля держите еще периодически обновляемый хеш для очереди… Вариант, да.
Кстати, конкретиные сообщения имею expiration? Есть ли возможность задать FIFO/LIFO?
друзья, разьясните по воводу директивы push_subscriber_concurrency, и более конкретно - о параметре broadcast, который подтверждает, что любое число клиентов могут слушать канал и одновременно каждый должен получить значение, которое передается. Т.е. говоря о данном скрипте в статье, мы можем отправлять в канал сообщение, и если канал слушают несколько клиентов, то сообщение получит каждый, в отличии от параметра last | first, где получит сообщение первый подключенный или последний.
Вопрос в следующем: при одновременной отправке через broadcast, из слушающих 4х клиентов реально получают сообщение только 2-3. т.е. не удалось достить 100% доставки для всех, в чем то есть затык, и пока не удалось понять в чём. Архитектура простая - 2 location в nginx: public, listener. Nginx проксирует запросы на apache через backend: 127.0.0.1:8080. Listener слушают 4е клиента, и когда на public через curl передается переменная, её значение получают почему то не все клиенты! в чем же может быть причина? все клиенты подключаются браузером с одной машины. буду очень благодарен за любые советы.
Спасибо за статью
>>Мы для каждого канала генерируем хеш ключ
Возникли пару вопросов:
1. Вы храните хеши в отдельной таблице (н-р, user_id, channel, expires)?
2. Когда вы его обновляете и как уведомляете слушателей о новом?
Буду благодарен за объяснения.
и еще вопрос:
как решить проблему, если на мой канал отправили почти одновременно два сообщения (первый до меня доходит, а второй - не дойдет)?
1. по первому вопросу: решил привязаться к session_id
2. по второму вопросу. есть список онлайн пользователей (user_id, session_id), поэтому для отправки сообщения пользователю, по его id достаю session_id. если таковая запись имеется, тогда
function send($cid, $data)
{
$code = 0;
$tries = 0;
$maxTries = 10;
while ($code != 201 && ++$tries <= $maxTries) {
$c = curl_init( ‘http://localhost/publish?cid=’ . $cid );
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
curl_setopt($c, CURLOPT_POST, true);
curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($data));
$r = curl_exec($c);
$code = curl_getinfo($c,CURLINFO_HTTP_CODE);
if($code != 201)
usleep(500000);
}
}
Имеется проблема, когда имеются два и более субскрайбера с одного браузера (в двух табах, к примеру). В такой ситуации, соединение при пуше отпускается строго по очереди, то для одного, то для другого.
Есть какие-то соображения?
Как можно получить список юзеров, слушающих канал? При отправке сообщения выводится только общее число подписчиков и количество сообщений в Message Storage.
Реализовал простенький чат, а вот теперь задача как организовать учет пользователей онлайн. Очень не хочется заставлять юзеров периодически отсылать post уведомления, что они еще слушают канал, а запрос на /listen не знаю как учесть.
Только не надо использовать модуль nginx для этого. Я приложил им тестовый сервак пятиметровыми посланиями в чат 10 раз в секунду.
Сначала подвисла очередь и сообщения стали приходить через 20 секунд,, потом сервер перестал отвечать. Подняли его через 3-4 часа. После этого они ограничили пакеты 32КБ.
С APE и перловым скриптом с либ евент это не прокатило.
@Aaa
APE, кстати, очень даже неплох, но только отъедает 70 метров оперативки и заметно юзает проц даже в режиме простоя, что как-то неправильно.