Используем 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 сервером так что дерзайте хотя. Надо бы протестировать это)