Привет!
Пару месяцев назад я опубликовал на Хабре статью, посвященную описанию open-source проекта Centrifuge. Напомню, что это сервер рассылки сообщений подключенным клиентам (в основном из веб-браузера) в реальном времени. Написан на Python.
С тех пор я продолжал работать над проектом в свободное время и сейчас готов поделиться накопившимися мыслями и изменениями.
Изначально, Центрифуга была самобытным проектом. Не сильно заботясь о воспроизведении функционала существующих аналогов, я писал код так, как казалось правильным мне самому. В итоге сообщения клиентам доставлялись, всё работало, но! Было ли удобно всем этим пользоваться? Нет!
В конце июня я наткнулся на великолепную статью от Serge Koval — Python and real-time Web. Удивительно, но на тот момент я не знал о существовании Faye. Статья открыла мне этот замечательный проект, как и понимание того факта, что все-таки Центрифуга в своем текущем состоянии не сильно упрощает жизнь при разработке real-time веб-приложений.
С того времени я допиливал Центрифугу с прицелом на удобство использования и с оглядкой на pusher.com, pubnub.com и Faye.
Вопрос, зачем мне нужно было писать код с нуля, если уже существуют более матерые и продвинутые аналоги, неизбежен. Вот несколько причин:
- Это интересно. В проекте на данный момент используются Tornado, ZeroMQ, Redis, SockJS, Bootstrap 3. Прекрасные инструменты, работать с которыми — безграничное счастье.
- Pusher.com и Pubnub.com — облачные сервисы, не всегда возможно/есть_смысл/хочется полагаться на третью сторону. Невозможно внести изменения в серверную часть.
- Аналогов на Python я так и не нашел (может быть вы знаете?), бэкенд Faye — это Ruby или NodeJS. Чтобы сделать авторизацию подписки в канал нужно писать расширения на этих языках. Я же хотел создать более независимое от языка бэкенда веб-приложения решение, предоставляющее необходимый функционал из коробки.
- Некоторые особенности, которых нет в аналогах. Это наличие пространств имен, определяющих особенности поведения принадлежащих им каналов. Веб-интерфейс для управления проектами, их настройками и возможностью в реальном времени следить за сообщениями в каналах.
Теперь расскажу об изменениях, произошедших с момента написания предыдущей статьи о Центрифуге.
Во-первых, структура — проекты, пространства имен и их настройки — теперь по умолчанию будут храниться в SQLite — базе данных, входящей в стандартную библиотеку Python. Поэтому при запуске процессов Центрифуги на одной машине больше нет необходимости в установке PostgreSQL или MongoDB, как было ранее. Так как Центрифуга рассчитана на использование в небольших и средних проектах — я считаю, это важное и нужное изменение, так как одной машины должно хватить сполна.
Можно пойти чуть дальше — и запустить Центрифугу со структурой, описанной в конфигурационном файле. При этом теряется возможность динамически вносить и сохранять изменения из веб-интерфейса, но зато нет никаких зависимостей от внешнего хранилища. Данная возможность также чрезвычайно помогает при разработке.
Появилась поддержка presence и history — теперь можно узнать, кто в данный момент подключен к каналу, а также получить последние сообщения, отправленные в канал. Для хранения этих данных используется Redis. Если Redis не настроен — данные просто не будут доступны клиентам, ничего при этом не сломается.
Возникает вопрос. Сейчас Центрифуга использует ZeroMQ PUB/SUB сокеты для коммуникации между несколькими своими процессами. Быть может, раз в игру вступил Redis в качестве хранилища информации о подключенных клиентах и истории сообщений, то стоит использовать его PUB/SUB возможности и для коммуникации между процессами Центрифуги вместо ZeroMQ? В том единственном сравнительном бенчмарке, который я видел, ZeroMQ по производительности опережает Redis.
Поэтому на данный момент я оставил все как есть. Однако это спорный и важный момент.
Еще теперь можно получать сообщения о подключении(отключении) клиента к каналу (от канала). Приятный пустячок.
Наконец, самое, пожалуй, важное — появился javascript-клиент — обертка над протоколом Центрифуги. Он построен на основе Event Emitter, написанного Оливером Калдвеллом (Oliver Caldwell). Теперь взаимодействовать с Центрифугой из браузера очень просто. Примерно вот так:
var centrifuge = new Centrifuge({
// настройки аутентификации
});
centrifuge.on('connect', function() {
// соединение с Центрифугой установлено
var subscription = centrifuge.subscribe('python:django', function(message) {
// функция, вызываемая при получении нового сообщения из канала
});
subscription.on(‘ready’, function() {
subscription.presence(function(message) {
// получена информация о подключенных к каналу клиентах
});
subscription.history(function(message) {
// история последних сообщений канала
});
subscription.on('join', function(message) {
// вызывается, когда новый клиент подключается к каналу
});
subscription.on('leave', function(message) {
// вызывается когда клиент отключается от канала
});
});
});
centrifuge.on('disconnect', function(){
// соединение с Центрифугой потеряно
});
centrifuge.connect();
За бортом в этом примере остались настройки аутентификации (о них можно прочитать в документации). Также обратите внимание на название канала — оно состоит из имени пространства имен, которое должно быть создано в административном интерфейсе до подключения, в данном случае это python
. Непосредственно имя канала указывается после — в данном случае это django
. Пространство имен определяет настройки всех принадлежащих ему каналов. В настройках проекта можно выбрать пространство имен по умолчанию — тогда в javascript-коде можно не указывать явно название пространства имен. То есть, в случае если пространство имен python является дефолтным для проекта, можно писать вот так:
centrifuge.on('connect', function() {
var subscription = centrifuge.subscribe('django', function(message) {
console.log(message);
});
});
Авторизация в такого рода приложениях, пожалуй, самая сложная часть. Как я уже упоминал, в Faye нужно писать расширения на NodeJS или Ruby для защиты доступа к определенным каналам. Pusher.com для приватных каналов предлагает следующую схему:
При попытке подписаться на приватный канал, отправляется AJAX запрос на бэкенд вашего приложения с именем канала. В случае, если доступ разрешен, вы должны вернуть подписанный ответ, который в дальнейшем вместе с именем канала отправляется непосредственно в Pusher. Преимущество здесь в том, что ваше приложение на момент получения AJAX-запроса в большинстве случаев уже содержит объект текущего пользователя (например, в Django это request.user
).
В Центрифуге применяется немного иной подход. Идентификатор текущего пользователя отправляется один раз в момент подключения — его вы указываете при конфигурации javascript-клиента вместе с ID проекта и токеном. Токен — это HMAC, сгенерированный на основе секретного ключа проекта (о котором должен знать только бэкенд вашего приложения), ID проекта и ID пользователя. Токен необходим для проверки корректности переданных ID проекта и ID юзера. В дальнейшем при подписке на приватные каналы Центрифуга будет отправлять POST запрос вашему приложению со строковыми ID юзера, именем пространства имен и именем канала. Поэтому первым делом в функции-обработчике авторизации вам нужно будет получить объект своего пользователя по ID.
Еще один важный момент, касающийся авторизации — сейчас, чтобы подписаться на несколько каналов приходится несколько раз вызывать функцию subscribe
на клиентской стороне. Если каналы приватные, то каждая такая подписка будет приводить к POST запросу к вашему приложению. Не оптимизированное поведение, которое хотелось бы улучшить. Но тот же pusher.com, признавая, что такие случаи хоть и редки, но бывают среди требований их клиентов, пока в полной мере эту проблему не решил. Здесь я пока в поиске правильного пути решения.
Хотелось бы отметить еще один способ защиты приватных данных, который никто не отменял. Например, чтобы сделать отдельные приватные каналы для каждого пользователя приложения — можно генерировать трудно угадываемые имена каналов на основе какого-либо секретного ключа и ID пользователя. В таком случае вполне можно обходиться без дополнительной авторизации, по крайней мере до тех пор, пока вашим клиентам не выгодно делиться именами своих приватных каналов:)
Есть возможность добавить кастомную асинхронную функцию (обрамленную tornado-декоратором @coroutine
) перед публикацией сообщения в канал. Внутри этой функции можно делать с сообщением все что угодно, в том числе вернуть None и тем самым отменить публикацию сообщения. Но, пожалуй, это мало кому пригодится, как и аналогичная возможность добавить обработчик, вызываемый после публикации. Это достаточно низкоуровневое вмешательство и требует знания Python и Tornado.
Установка Центрифуги в самом простом случае сводится к одной команде pip install centrifuge
внутри virtualenv. Однако на машине должен быть установлен ZeroMQ (libzmq3) и dev-пакет для PostgreSQL (сам PostgreSQL сервер необязателен). Найденные проблемы, которые могут возникнуть при установке из PYPI, и способ их решения описаны в документации. Запуск одного процесса выполняется командой centrifuge
. Однако для запуска в боевую среду потребуется конфигурационный файл, так как в нем содержатся важные настройки безопасности. Также не обойтись без использования дополнительных опций командной строки, если вы хотите запустить несколько процессов.
Вот в этом разделе документации я постарался как можно подробней объяснить как работает Центрифуга, какие есть опции запуска, какой адрес указывать при подключении из браузера и многое другое. На английском, правда.
Нагрузочное тестирование пока не проводил. Надеюсь, займусь бенчмарками в ближайшее время. Интересно сравнить с Faye, интересно запустить на PYPY. Ну и, конечно, необходимо продолжать работу над устойчивостью к всевозможным ошибкам, совершенствовать Python-код и javascript-клиент и так далее. Присоединяйтесь!
Спасибо за внимание!
Автор: FZambia