Успешная миграция на собственное решение XMPP
Опубликовано: 2019-04-08Я собираюсь рассказать вам о проблемах, с которыми мы столкнулись при переходе со стороннего чата на собственное решение для обмена сообщениями на основе XMPP для нашего клиента, Forward Health, решения для обмена сообщениями в Великобритании. В этой статье будут рассмотрены причины миграции, наши ожидания и реалии реализации, а также проблемы создания дополнительных функций.
С чего мы начали
Forward Health, наш клиент, хотел создать мобильное коммуникационное приложение для медицинских работников в Великобритании, включая функцию чата. Будучи стартапом, они хотели быстро показать свой работающий продукт. В то же время обмен сообщениями должен был быть надежным, надежным и обеспечивать безопасную передачу конфиденциальных данных пациентов. Для этого мы решили использовать одно из доступных сторонних решений для функционала чата .
Функционал чата — вещь нетривиальная, особенно когда он направлен на поддержку отрасли здравоохранения. По мере роста приложения мы сталкивались с большим количеством пограничных случаев и некоторыми ошибками на стороне библиотеки, над которыми сторонние разработчики не хотели работать. Кроме того, Forward Health хотела добавить новые функции, которые не поддерживались сторонней библиотекой. Следующим шагом стал переход на индивидуальное решение.
Именно тогда мы начали работать с MongooseIM . MIM — это решение с открытым исходным кодом, основанное на хорошо зарекомендовавшем себя протоколе XMPP. Мы работали с внешней компанией Erlang Solutions Limited, чтобы настроить нашу серверную часть и обеспечить поддержку при внедрении пользовательских решений.
Сначала все в обмене сообщениями казалось другим. Раньше все наши потребности удовлетворялись с помощью SDK и его REST API. Теперь, используя MongooseIM, нам пришлось потратить некоторое время, чтобы понять природу XMPP и реализовать собственный SDK . Оказалось, что «голый» сервер XMPP передавал только строфы (XML-сообщения) между клиентами в режиме реального времени. Станцы могут быть разных типов, т.е. обычные сообщения чата, присутствие, запросы и ответы. На сервер можно добавить множество модулей, например, для хранения сообщений и предоставления клиентам возможности запрашивать их.
На стороне клиента (Android, iOS) были какие-то низкоуровневые SDK. К сожалению, они действовали только как уровень, обеспечивающий связь с MongooseIM и некоторыми из его подключаемых модулей, называемых XEP (протокол расширения XMPP, отвечающий, среди прочего, за отправку push-уведомлений для каждого сообщения). Наша команда должна была реализовать всю архитектуру обработки, хранения и запроса сообщений.
Нам на помощь пришла сторонняя библиотека , которую мы использовали ранее. У него был очень продуманный API, поэтому мы сделали наше решение похожим. Мы выделили специальный код XMPP в наш внутренний SDK с интерфейсом, соответствующим интерфейсу из предыдущего решения. Это привело лишь к нескольким изменениям в коде нашего приложения после миграции.
Во время внедрения MongooseIM мы несколько раз удивлялись элементам, которые, как мы думали, будут стандартными, но недоступными для нас даже в XEP.
Реализация ключевых функций чата на основе XMPP
Временные метки
Вы можете подумать, как и мы, что временные метки будут такими же простыми, как «Я получаю сообщение, я отображаю это в пользовательском интерфейсе с временной меткой». Нет, не так просто. По умолчанию строфы сообщений не имеют поля метки времени. К счастью для нашей команды, XMPP — легко расширяемый протокол. На бэкенде мы реализовали пользовательскую функцию, добавляя метку времени к каждому сообщению, прошедшему через сервер MongooseIM. Тогда получатель будет иметь временную метку, прикрепленную к сообщению.
Почему отправитель не может сам добавить метку времени? Ну, мы не знаем, правильно ли они установили время на своем телефоне.
Почему для этого нет XEP? Может быть, потому что XMPP — это протокол реального времени, поэтому теоретически каждое отправленное сообщение будет получено сразу же.
РЕДАКТИРОВАТЬ: Как заметил Флориан Шмаус: «На самом деле есть один, хотя его легко пропустить из-за его запутанного названия: XEP-0203: Delayed Delivery». Он добавляет метку времени к сообщению, только если его доставка задерживается. В противном случае сообщение было отправлено только что.
Офлайн-сообщения
Когда оба пользователя вошли в приложение, они могут отправлять сообщения друг другу в режиме реального времени. Но что, если один из них не в сети? Быстрый ответ: сообщения должны буферизоваться на бэкэнде . Функция автономных сообщений выполняет эту работу и отправляет все буферизованные строфы пользователю после его повторного входа в систему.
Но тогда возникает несколько вопросов:
- Как долго эти сообщения должны храниться в буфере?
- Сколько из них?
- Должны ли они быть повторно отправлены сразу после входа в систему? Но это завалит клиента сообщениями, не так ли?
- Что делать, если пользователь только авторизуется, но не входит в чат с новыми сообщениями. Все ли они исчезнут?
- Что делать, если пользователь авторизовался на нескольких устройствах?
Стало очевидным, что функция автономных сообщений могла отправлять сообщения только на первое устройство, которое вернулось в сеть, и эти сообщения затем были потеряны для всех других устройств. Мы решили отказаться от этой функции и хранить сообщения на серверной части XMPP другим, постоянным способом.

Управление архивом сообщений (MAM)
MAM — это хранилище сообщений на сервере. Когда клиент входит в систему, он может запрашивать сообщения у сервера. Вы можете запрашивать по страницам, вы можете запрашивать по датам. Это гибко — вы даже можете запрашивать страницу до или после сообщения с определенным идентификатором, добавляя фильтры для сообщений из конкретной беседы.
Но вот загвоздка. Обычные сообщения чата хранятся внутри сообщений MAM, которые имеют собственные уникальные идентификаторы. Когда пользователь получает сообщение чата в потоке, оно не содержит MAM ID. Они должны запросить MAM, чтобы получить его.
Получение из MAM — это сетевой запрос, что означает, что это может занять относительно много времени. Когда пользователь входит в чат, он хочет немедленно видеть сообщения. Так что нам также нужна локальная база данных .
Когда пользователь получает сообщение в потоке (онлайн-сообщение), мы сохраняем его в локальную базу данных и показываем пользователю. Таким образом, мы отображаем сообщения, которые быстро поступают пользователю в режиме реального времени.
Кроме того, каждый раз, когда они входят в экран чата, мы загружаем все сообщения с этого момента в новейшее сообщение MAM, хранящееся в локальной базе данных для этого разговора, и помещаем их в базу данных, игнорируя дубликаты.
Вот как мы справляемся с хранением старых сообщений. Также мы уверены, что в базе есть полный набор сообщений для конкретного разговора между первым и последним сообщением от МАМ.
Чтобы отслеживать сообщения, загруженные из MAM, мы добавили два свойства к объектам беседы:
- Идентификатор MAM самого нового сообщения MAM в базе данных
- Идентификатор MAM самого старого сообщения MAM в базе данных
Обработка разрозненных наборов сообщений MAM в локальной базе данных была бы очень проблематичной.
Кроме того, наличие этих двух свойств для каждого разговора позволяет нам хранить обычные сообщения чата в базе данных, игнорируя оболочку — сообщение MAM. И когда пользователь входит в чат, мы можем показать последние сообщения из базы данных и в фоновом режиме получить недостающие сообщения из MAM.
Входящие
Каждому чат-приложению нужен экран со списком чатов — место, где вы можете увидеть имена, последние сообщения и количество непрочитанных сообщений. Там должно быть решение!
На самом деле, нет… Есть что-то под названием Roster — в нем может храниться список пользователей, помеченных как «друзья». К сожалению, к ним нет ни последнего сообщения, ни счетчика непрочитанных сообщений. Конечно, вы можете получить необходимую информацию из бэкенда по частям. Сначала мы хотели сделать так, но это будет работать медленно и будет сложно. Именно тогда мы начали работать с Erlang Solutions над функцией Inbox, исходный код которой также становится открытым.
Когда пользователь подключается к серверной части XMPP, приложение извлекает его папку «Входящие», которая содержит все разговоры этого пользователя — как индивидуальные, так и групповые чаты. К каждому из них прикреплено последнее сообщение и количество непрочитанных сообщений. Приложение сохраняет весь почтовый ящик в локальной базе данных. Когда пользователь находится в приложении и приходит новое сообщение, мы локально обновляем состояние папки «Входящие». Таким образом, приложению не нужно извлекать папку «Входящие» для каждого нового сообщения.
Резюме
Некоторые сторонние решения для чата обеспечивают высокий уровень абстракции. Это нормально, если вы хотите создать простое приложение для чата. Внедрив наше собственное решение на основе XMPP в приложение Forward, мы смогли получить гораздо лучший низкоуровневый доступ, что значительно упростило решение проблем. Конечно, это заняло некоторое время, но теперь мы знаем, что можем предоставить любую пользовательскую функцию, чтобы помочь врачам в Великобритании общаться безопасным и простым способом, одобренным Национальной службой здравоохранения.
Обмен сообщениями — это высокая производительность и общение в режиме реального времени. Перейдя на MIM, мы смогли оптимизировать каждую часть решения, чтобы повысить скорость, надежность и, в конечном итоге, доверие. В настоящее время у нас есть весь код, поэтому их легко отследить. Кроме того, мы прошли этап стабилизации, и количество отчетов, связанных с обменом сообщениями, резко сократилось. Пользователи довольны тем, что могут доверять платформе.
Проектирование и написание собственного SDK было непростой задачей, и нам это понравилось. Это было чем-то отличным от простых приложений, где вам нужно получать данные с сервера и отображать их на экране. Во время реализации мы поняли многие варианты дизайна API сторонних библиотек, которые мы использовали ранее. Почему? Потому что мы столкнулись с теми же проблемами.