Чаты на вебсокетах. Теперь про бэкенд
Продолжаем рассказывать про чаты на вебсокетах, но уже со стороны бэкенда. Когда-то использовали сторонний сервис, но было важно решить ряд моментов, которые он не мог покрыть. Выбирать особо не пришлось, и мы принялись разрабатывать собственное решение.
Ниже подробности о том, что было до написания кастомных чатов и какие стояли требования к реализации, из каких компонентов они состоят, как вписываются в нашу инфраструктуру и что получилось в итоге. А в конце статьи — ссылки про особенности разработки наших чатов на вебсокетах для iOS и Android.
Почему решили писать свои чаты
iFunny — приложение с лентой мемов и возможностью флуда в комментариях, но последней опции было мало по очевидным причинам. Например, людям хочется кидать мемы в личку и создавать тематические каналы. Чтобы не вынуждать их переходить в сторонние мессенджеры, мы решили сделать чаты внутри приложения.
Самый простой способ — использовать сторонний готовый сервис. На старте таким оказался Sendbird — платформа для создания чата внутри приложения, в которой есть все основные фичи современных мессенджеров. Основной плюс такого подхода:
Скорость внедрения. Как заявляет производитель, с нуля до первого сообщения — считанные минуты, так как не нужно писать бэкенд, только UI. Правда, для внутренних фич нам всё равно пришлось написать много бэкенд-кода (в основном это связано с модерацией открытых чатов и синхронизацией пользователей, а также у нас есть рейтинг открытых чатов).
Минусы, куда же без них:
Высокая цена.
Слабый контроль за происходящим внутри (пользователи доставали токен из мобильного приложения и спамили через скрипты, делали ботов).
Невозможность внутри сделать фичи под потребности.
Нельзя провести А/B-тест.
Ограничение на 500 пользователей в чате.
Для нас минусы оказались весомыми, поэтому решили разрабатывать свой чат, адаптировав под текущие задачи.
Основным техническим требованием было сделать всё то же самое, что у Sendbird, только без минусов. А также по возможности не использовать технологии, которые ещё не интегрированы в стек.
Подготовка
Выбор транспорта
Были мысли и предложения использовать UDP. Он обеспечивает более высокую скорость работы по сравнению с TCP, потому что не имеет накладных расходов на установку и на разрыв соединения. Но у нас не миллионы пользователей онлайн одновременно, а опыта работы с UDP у меня и у клиентских разработчиков было не так много, поэтому такой манёвр мог стоить неопределённого количества времени.
Для транспорта прикладного уровня выбрал WebSocket. Он давно везде поддерживается, и можно в будущем сделать чат в веб-версии приложения без правок на стороне бэкенда. А так как для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP, можем использовать тот же механизм авторизации, что и в HTTP API приложения — через заголовок Authorization.
Выбор протокола
На ум сразу пришёл XMPP. Это открытый, основанный на XML, свободный для использования протокол мгновенного обмена сообщениями. Основными преимуществами являются децентрализация, открытость стандарта и расширяемость. Из недостатков — невысокая эффективность передачи данных за счёт XML-формата. А наличие большого количества открытых клиентов может привести к оттоку пользователей из приложения.
Из открытых XMPP-серверов самым продвинутым показался ejabberd, написанный на Erlang. На такой подвиг я был не готов, кроме того, сам по себе протокол старый, и его пришлось бы расширять под свои нужды.
Также смотрел в сторону MQTT — простого сетевого протокола, построенного по модели pub-sub. Есть возможность пустить поверх WebSocket-соединения.
До последнего думал взять MQTT, но потом наткнулся на WAMP. Это открытый протокол, который поддерживает два шаблона обмена сообщениями: pub-sub и routed RPC.
Выбор инструментов
В качестве языка был выбран Kotlin JVM, а базы данных для хранения сообщений — MongoDB. Просто потому, что это наш основной стек.
Реализация
Начал с реализации WAMP-протокола. Не найдя в открытом доступе достойных реализаций роутера на Java/Kotlin, написал свою. По сути, это движок и сердце сервера.
Далее сделал минимально функциональный прототип, где можно было публиковать и получать сообщения. На этом этапе ещё не было базы, сообщения жили в памяти до перезагрузки сервера, а функционал ограничивался созданием чата и публикацией сообщений. Но уже можно было приступать к интеграции с мобильными клиентами.
Для загрузки медиафайлов используется отдельный HTTP-эндпоинт, чтобы ничего не изобретать.
Перенос данных
В Sendbird есть механизм вебхуков. В настройках аккаунта приложения можно указать эндпоинт, и Sendbird будет слать туда информацию обо всех событиях, происходящих в приложении. Перед тем как приступить к реализации, мы сделали скрипт, который принимает такие события и складывает их в базу данных. А когда пришло время, проиграли эти события на базу данных наших чатов. Оставшиеся пробелы докачали через Platform API Sendbird по HTTP.
Отдельно можно отметить перенос медиа. Нужно было выкачать все картинки и видео, пережать их под наши стандарты и сохранить на свой S3. Процедура достаточно затратная по времени, поэтому после запуска чатов в продакшн ещё какое-то время медиа грузилось с CDN Sendbird, пока скрипты в фоне выкачивали его в наш сторадж.
Запуск
На мобильных клиентах новая реализация была спрятана за фиче-тогглом. И когда новые версии разъехались на большинство пользователей, а бэкенд был поднят, включили фичу и чаты заработали через наши сервера.
Приложение для миграции было устроено таким образом, что импортировало не только исторические данные, но и те, которые шли в реальном времени. То есть пользователи, уже использующие новый чат, могли видеть сообщения от тех, кто ещё был в Sendbird.
Архитектура бэкенда
Бэкенд состоит из двух сервисов — непосредственно сервис чатов, с которым клиент соединяется по вебсокет при переходе на вкладку «чаты» в приложении, и сервис хуков, у которого две роли:
Принимает вебхуки от чатов и делает дополнительную логику:
генерирует пуши;
обновляет поисковый индекс открытых чатов;
поддерживает коллекции, необходимые для модерации;
удаляет старые каверы с S3;
считает рейтинг открытых чатов для ранжирования в приложении.
Служит HTTP-proxy для внутренних RPC-вызовов по WAMP-протоколу. Этим пользуется php-монолит для:
получения счётчика непрочитанных сообщений и инвайтов для шторки меню в приложении;
получения информации о чатах и сообщениях в админке;
асинхронной загрузки медиа в чаты.
Вместо заключения
Это то, что касается бэкенда. Про клиентскую сторону, а именно особенности разработки чатов на вебсокетах для iOS и Android можно узнать из материалов коллег, опубликованных ранее.
Там подробнее про поддержку старых версий операционных систем, способы декодирования (которые можно применить и в других задачах) и особенности работы с WAMP.