Привет!
В статье я опишу свой небольшой open-source проект — Centrifuge (далее Центрифуга). Это сервер на Python, задача которого — рассылка (broadcast) сообщений в реальном времени подключенным (в основном из браузера) клиентам.
Это будет история, наполненная как личными эмоциями, так и описанием используемых технологий, но без примеров кода. Если вам близка тема — не проходите мимо, будет любопытно.
Для начала, посмотрите, пожалуйста, скринкаст (не забудьте включить субтитры), если после просмотра интерес не пропадет, смело читайте дальше!
Идея сервера real-time сообщений совсем не новая, среди существующих подобных проектов могу привести в пример Pusher и Pubnub. Вот цитата с сайта Pusher:
Pusher is a hosted API for quickly, easily and securely adding scalable realtime functionality to web and mobile apps.
Pubnub на своей главной страницы говорит нам нечто похожее:
Thousands of mobile, web, and desktop apps rely on the PubNub Real-Time Network to deliver highly scalable real-time experiences to tens of millions of users worldwide.
Цели Центрифуги далеко не такие глобальные. Это не готовая распределенная по миру инфраструктура для создания реал-тайм приложений, это просто сервер, который вы устанавливаете себе на машину и используете в качестве брокера сообщений.
Не сомневаюсь, что подобных серверов существует немало. Мне и самому некоторое время назад довелось написать очень похожую штуку — cyclone-sse. Это демон на Twisted, который позволяет рассылать сообщения по каналам в реальном времени, используя технологию Server-Sent Events (или откат (fallback) до Long-Polling для старичков вроде IE 7). Получился вполне приличный кусок кода, который мы успешно используем в бою.
Однако тот демон не решает некоторых важных проблем:
1) Отсутствие какой-либо авторизации. В нашем случае все проекты закрыты извне файрволлом компании и cyclone-sse мы используем только для публичных данных. Но чтобы добавить реал-тайм события в проект, который доступен всем пользователям интернета, нужен механизм авторизации.
2) Существует более быстрая чем Server-Sent Events реализация передачи сообщений — Websockets. Технология, которая помимо увеличения производительности, предоставляет еще и возможность двустороннего обмена данными (в то время как SSE — это однонаправленный протокол, сообщения возможны только от сервера клиенту). К тому же вебсокеты поддерживают кросс-доменное общение, что не всегда справедливо для Server-Sent Events.
Однажды коллега по работе пожаловался, что ему не хватает возможности мониторить обновления пакетов для JAVA. Я подумал, что это неплохая идея для небольшого проекта — отслеживать новые пакеты, возможно не только для Java, но и для других языков программирования, в реальном времени показывать обновления в веб-интерфейсе — и приступил.
Чем больше я писал, тем дальше уходил код от изначальной задумки. Из аггрегатора обновлений пакетов проект превратился в аггрегатор всего.
Звучит сильно, но спустя некоторое время я понял, что на реализацию подобной задачи нужно нечто большее, чем есть у меня… И было принято решение все упростить — на тот момент уже был написан скелет рассылки сообщений подключенным клиентам, почему бы не развивать это направление? Я понял, что смогу написать код, похожий по назначению и области применения на cyclone-sse, но более приспособленный к массовому использованию.
Итак, вы python-программист и вам нужно написать такой вот сервер — что вы будете использовать? На выбор Twisted, Gevent и Tornado. И, пока Гвидо Ван Россум стандартизирует интерфейс event-loop программ в библиотеке Tulip, нам нужно выбирать.
Я выбрал Торнадо. Он работает на третьем питоне, он просто классный в конце концов. Во многом этот выбор и желание того, чтобы конечный код работал на Python 3.3, предопределил выбор остальных сопутствующих технологий — ZeroMQ (pyzmq), SockJS (sockjs-tornado), MongoDB (motor) и PostgreSQL (momoko). Все библиотеки — асинхронные, исключающие блокировки при взаимодействии с сокетами.
К ZeroMQ я пришел не сразу.
Когда есть несколько процессов приложения за балансировщиком, и клиенты теоретически могут подключиться к любому из них — необходимо каким-то образом поддерживать целостность внутреннего состояния такой системы и иметь возможность коммуникации между экземплярами приложения. Изначально для этих целей я использовал Pub/Sub механизм Redis. Но наткнулся на баг в реализации библиотеки Tornado-Redis и посмотрел в сторону других решений.
В итоге выбор пал на ZeroMQ — сокеты на стероидах, набор паттернов для организации самых разнообразных сетевых взаимодействий. Отсутствие отдельного брокера — это просто великолепно. Если вы еще не слышали об этой библиотеке, или слышали, но не вдавались в подробности — исправляйтесь сейчас же! Прочитайте их The Guide, оно того стоит. Это мой первый проект с использованием данной библиотеки, надеюсь, опытные участники сообщества посмотрят на код и укажут на возможные недочеты.
Каждый процесс Центрифуги создает PUB сокет, который биндится на определенный адрес/порт. Процесс также имеет SUB сокет, который соединяется с PUB сокетом текущего процесса и PUB сокетами остальных инстансов (если таковые запущены). Минусом такой схемы является необходимость вручную указывать все адреса PUB сокетов при запуске процесса. Поэтому есть возможность запустить XPUB/XSUB прокси в отдельном процессе и запускать все процессы Центрифуги с использованием этого прокси. То есть организовать все взаимодействие вот по такой схеме:
Последняя часть головоломки — клиентская. Tornado из коробки работает с вебсокетами, но я решил пойти чуть дальше и позволить клиенту использовать еще и SockJS. Так что всё будет работать и в браузерах без поддержки вебсокетов. Хотелось бы отдельно поблагодарить Serge S. Koval (mrjoes) за sockjs-tornado. Поддержка socket.io не планируется.
Итак, что в итоге? А что-то вроде этого:
Как видно на диаграммке, в качестве базы данных используется MongoDB или PostgreSQL. Что нам хранить? Проекты и их настройки, категории внутри этих проектов. Подробнее об этом расскажу чуть ниже.
Мне кажется, я до сих пор так и не смог внятно объяснить, что же такое я тут всем впариваю. Итак, вот примерно так, по пунктикам:
1) Вы захотели добавить на свой сайт нечто реал-таймовое — комментарии, графики, обновляющиеся счетчики, уведомления…
2) Однако ваш сайт не на асинхронном бэкенде, или на самом что ни на есть асинхронном, но вам не хочется писать с нуля логику менеджмента каналов, подписок и т.д.
Центрифуга вполне может подойти вам в таком случае.
3) pip install centrifuge. Или чуть более подробно в документации (документация пока скомканная, местами непонятная, но в будущем надеюсь изменить ее к лучшему).
4) Интегрировать все же придется… Подчеркну некоторые важные моменты.
Для начала, нужно Центрифугу запустить. Да, там много всяких параметров для запуска, но я верю, что у вас получится, а если нет — пишите мне, я помогу.
После запуска нужно зайти в административный веб-интерфейс, создать новый проект. У проекта есть несколько настроек, описывать в этой статье их не буду — текста и так получается слишком много.
После создания проекта добавьте в него категории — по сути это пространства имен в проекте, внутри которых существуют каналы. Так как каналы создаются на лету, категории играют роль хранилища настроек для каналов, а также существуют для ограничения прав на подписку к тому или иному каналу внутри категории.
Самая, пожалуй, важная опция для категории — bidirectional. Если отметить ее галочкой, то подключившиеся клиенты смогут сами посылать сообщения в канал, без задействования вашего приложения. Иначе, только однонаправленное сообщение от сервера к клиенту — когда ваше приложение отправляет с помощью POST запроса событие в Центрифугу. POST запрос содержит данные о проекте, категории, канале и непосредственно пересылаемое сообщение. Сообщение рассылается всем подписанным на канал клиентам.
Итак, Центрифуга (или несколько Центрифуг) крутит свой event-loop, проекты и категории созданы, дело за клиентской частью. Как я уже говорил, для общения можно использовать нативные вебсокеты или библиотеку SockJS. На данный момент не существует javascript-библиотек, которые упрощают взаимодействие с Центрифугой, возможно, они появятся в будущем. Пока, чтобы взаимодействовать, нужно отправлять JSON сообщения, соответствующие JSON-схемам. На данный момент существуют только 4 метода для таких команд:
- auth — первое сообщение после установки соединения — авторизация.
- subscribe — после успешной авторизации можно подписываться на каналы в различных категориях, на которые был получен доступ во время авторизации.
- unsubscribe — отписаться от канала
- broadcast — отправить сообщение в канал, работает только для каналов, принадлежащих двунаправленной (bidirectional) категории
Чтобы не позволять всем и каждому подключаться к каналам Центрифуги, используется симметричное шифрование на основе секретного ключа. Этот ключ можно увидеть в веб-интерфейсе в настройках проекта. Ваше веб-приложение должно уметь генерировать специальный токен (вот так) на основе этого секретного ключа проекта.
При подключении нового клиента, если вы указали в параметрах проекта специальный адрес — Центрифуга отправит POST запрос на этот адрес с данными подключающегося клиента, ваше приложение должно на основе этих данных определить, имеет ли данный клиент доступ к запрашиваемым разделам, и если авторизация позволена — вернуть соответствующий ответ Центрифуге.
Я опустил очень много технических подробностей. Специально не вставил ни единого кусочка кода — проект очень молодой и, кто знает, что изменится в ближайшем будущем, хотя бы после комментариев к этой статье. За кадром остались взаимодействие с Центрифугой с помощью специального клиента Cent, детальное описание авторизации клиентов, разбор параметров команд для взаимодействия из браузера, описание опций проектов и категорий. Думаю, это было бы слишком утомительно. Если к проекту будет какой-либо интерес — напишу об этом в будущем.
В репозитории на Github есть пример приложения, использующего Центрифугу. А еще в документации есть пример конфигурации Nginx для деплоя. Лицензия — BSD. Если по каким-либо причинам вы хотите, но не можете использовать Centrifuge из-за лицензии — напишите мне, я пересмотрю.
Жду ваших замечаний и предложений по улучшению.
Автор: FZambia