Как мы выбирали идеальный протокол для мессенджера в ЕЦП.МИС (Медицинской информационной системе)
У нас в «РТ МИС» уже был мессенджер для ЕЦП.МИС. Ну, как «мессенджер» – некий самописный сервис на Node.js и хранением сообщений в БД для общения врачей и групповых уведомлений типа «Терапия! Тортики в ординаторской, успевайте».
В один прекрасный день мы собрались и решили, что все, хватит: нам нужен новый продукт, чтобы было «модно и молодежно» и еще куча функций в придачу. Дорабатывали тогда как раз модуль Стационара – здесь и приемное отделение, куда постоянно кого-то привозят и надо отправлять уведомления врачам, и большой поток информации по результатам анализов, какие-то показатели пациентов, консилиумы и вот это все. Да еще где-то впереди маячили доработки Поликлиники с телемедицинскими консультациями и уведомлениями по статусам талонов на прием.
Основные задачи для мессенджера
текстовый чат пользователь – пользователь, обычно – это врач – врач или врач – медсестра;
различные уведомления пользователей, например, врача о появлении результатов анализов или прибытии пациента в приемное отделение;
текстовый чат в контексте пациента или случая лечения, когда врач может задать какие-то уточняющие вопросы другим врачам с привязкой к случаю лечения или пациенту;
передача файлов в сообщениях;
работа с историей сообщений (поиск, просмотр, отметки о прочтении).
В перспективе
различные сценарии уведомления, например, уведомление врачу при смене статуса талона на запись в поликлинику, рассылки по должностям, структурным подразделениями;
«пейджер» – push-уведомления врачу на его личное мобильное устройство в случае, если врач не подключен к внутренней сети;
аудио-видео конференции (консилиум);
телемедицинские консультации, в том числе при участии пациента, авторизованного через Госуслуги;
Основные пользователи системы сообщений на данном этапе – врачи и средний медперсонал. Пациенты тут никак не задействованы, за исключением в перспективе их участия в телемедицинских консультациях, авторизовавшись через Госуслуги.
Вперед, на поиски!
Когда речь заходит о каких-то системах сообщений, «олдфаги» вспоминают IRC и ICQ. Если надо «модно и молодежно» – речь заходит о Discord и Slack. Поклонники приватности берут Matrix. Все остальные используют Telegram. Вроде бы бери и пользуйся. Но для наших целей это все абсолютно неприменимо: медицинская информация, с которой работают врачи, – это такой забористый коктейль из персональных данных и медицинской тайны, что требует особых подходов, в частности:
практически вся работа идет в защищенном контуре, куда нет доступа посторонним сервисам;
персональные данные и другая чувствительная информация должна храниться и обрабатываться в РФ;
нежелательно использовать каких-то иностранных поставщиков.
В общем, все это резко ограничивает набор возможных решений.
А чего, собственно, хочется от мессенджера?
открытость – отсутствие vendor lock in, открытый код;
контроль – возможность развернуть self-hosted инсталляцию;
наличие реализаций (клиентов) под основные языки (платформы), используемые у нас: Java, PHP, Node.js, Python;
стабильность - технология должна пройти фазу «хайпа»;
развитие – технология не должна быть «мертвой»;
шифрование – хорошо, но не в первую очередь, вся работа идет в защищенном контуре;
интеграция – возможность встраивания в существующие системы, в частности, авторизация и список пользователей;
расширяемость – некий подход для создания расширений в протоколе/ПО без «глобальных костылей».
Где-то тут мы стали понимать, что Телеграм, Дискорд и прочий Слак нас не спасут. Нужно переходить в область Open Source. Задачу осложняло то, что нужно было интегрироваться с нашими существующими системами и сервисами, а еще требовалась передача различной специфики по случаю лечения (врач, срочность, тип диагноза и т.д.). Можно было бы придумать свой формат сообщений – обмениваться json-ами и передавать всю необходимую специфику в полях, но хотелось не терять возможности работы с какими-то «стандартными» клиентами без наших доработок для облегчения интеграции сторонних модулей.
Что делать? Продолжать пилить что-то свое или все же есть выход?
Безусловно, одним из возможных путей было оставить все как есть и продолжать развивать собственное решение. Тем более что на тот момент уже кроме чатов была в каком-то виде реализована поддержка аудиоконференций. Альтернативным направлением поиска стал переход от готовых решений в область протоколов.
Беглый поиск показал активное развитие различных децентрализованных протоколов, например, Matrix, Signal. Но по ряду параметров, в частности, возможность работы с историей, это нам не подходило. Что-то стало откровенной экзотикой (OSCAR). Или более относилось в категорию «мессенджер» чем протокол (Mattermost). И тут кто-то вспомнил про XMPP. На самом деле у нас уже был опыт использования XMPP, но в качестве… корпоративного мессенджера. Как раз в тот период, когда ICQ уже перестала быть популярной, а что-то более продвинутое еще не набрало нужной популярности. В последствии, уже в «наше время» мы вторично пытались его использовать, но несмотря на интересные фишки, которые там появились, наши технические специалисты не смогли (или не захотели) толком все настроить и XMPP проиграл гонку какому-то платному решению.
Краткое введение в XMPP
XMPP
eXtensible Messaging and Presence Protocol – «расширяемый протокол обмена сообщениями и информацией о присутствии», ранее известный как джа́ббер. Открытый, основанный на XML, свободный для использования протокол для мгновенного обмена сообщениями и информацией о присутствии в режиме, близком к режиму реального времени. Изначально спроектированный легко расширяемым протокол, помимо передачи текстовых сообщений, поддерживает передачу голоса, видео и файлов по сети.
JID
Jabber Identifier строится по тому же принципу, что адрес электропочты: имя@домен. Может быть записан в краткой форме имя@домен (bare JID) или в полной (full JID) имя@домен/ресурс. Ресурс служит для того, чтобы можно было различить нескольких клиентов, подключенных к одной учетной записи. У каждого клиента ресурс должен быть уникальным. Тогда мы можем выбирать послать сообщение только одному клиенту или всем сразу. JID может быть не только у пользователя, но и у чат-комнаты, подписки и т.д. Для ресурса вводится понятие «приоритета» – если сообщение будет отправлено на краткий JID то оно будет доставлено тому клиенту, приоритет которого выше (или всем, если приоритет у всех одинаковый).
Станза (строфа)
Законченный элемент XML-потока, который содержит определённую управляющую информацию:
информация о присутствии (Presence) — информационные пакеты специального вида, которые содержат в себе информацию о том, подключен ли в данный момент определенный JID к сети Jabber, а также передаёт его статус, статусное сообщение и приоритет;
IQ (Info/Query) – особый вид стансов, реализующий механизм типа «запрос-ответ». Интерпретация IQ-станс позволяет «сущности» сделать запрос и получить ответ от другой «сущности». Тип данных, передающихся в запросе или ответе определяет пространство имён (namespace) дочернего элемента по отношению к IQ;
сообщение (Message) – используется для обмена сообщениями между пользователями. Выглядит примерно так:
<message from="doctor_maria@example.ru/Desktop" to="doctor_anna@example.ru" type="chat"><body>Привет, как дела?</body></message>
Ростер (список контактов)
Разбитый на группы список Jabber-адресов ваших собеседников (контактов). Хранится на сервере и передаётся клиенту по запросу. Сервер также обрабатывает запросы на добавление, удаление контакта из списка, а также смены группы для конкретного контакта.
XEP (расширения)
XMPP Extension Protocol — расширение протокола XMPP. Например, XEP-0045 – многопользовательский чат, XEP-0084 – поддержка аватарок пользователей, XEP-0107 – статус пользователя (user mood). XEP описывают как какие-то базовые вещи (XMPP Core), так и множество продвинутого и очень интересного функционала.
Вообще, расширения – одна из самых интересных особенностей XMPP, когда, используя кирпичики описанные выше (различные стансы), мы описываем необходимый нам функционал. При желании можно сделать и собственное расширение. В настоящий момент насчитывается порядка двух сотен действующих XEP.
Использования XMPP
Открытое тестирование XMPP, июль 2010 Отключение сторонних XMPP-клиентов, июль 2013 | |
В Одноклассниках появилась поддержка Jabber, октябрь 2011 | |
NSA использует тот же протокол что хакеры и активисты, декабрь 2014 | |
В связи с переходом на Platform API 2.0 прекращается поддержка XMPP Chat API, июль 2015 | |
Используется FunXMPP – доработанная версия, октябрь 2017 | |
Перешли с собственного решения на ejabberd, февраль 2018 | |
Используется XMPP, июль 2019 | |
Чат Zoom основан на стандарте XMPP, апрель 2020 |
Для всех этих случаев можно выделить одну картину (особенно характерную для больших порталов): быстрый старт сервисов, используя XMPP, а затем, когда вступает в игру коммерческая составляющая и стоит задача привязать пользователя к порталу, уже рождаются какие-то собственные решения, возможно, остающиеся в своей массе основанными на XMPP.
Список компаний и решений внушал, задачи «зарабатывать с пользователя» перед нами не стояло, и мы уже были готовы бежать и делать все на XMPP. Но тут выяснилась одна особенность: для XMPP необходимо рассматривать не только протокол, но в большей степени сервер и клиента, что его реализуют. А все потому, что набор реализуемых расширений (тех самых XEP) от сервера к серверу могут различаться.
Выбираем сервер
Самыми часто упоминаемыми серверами XMPP являются (в скобках – язык реализации):
Когда вы читаете про десятки и сотни тысяч пользователей, которых держит один XMPP-сервер, скорее всего, речь идет о Ejabberd. Но мы сразу понимали, что возможны доработки, а специалистов по Erlang среди нас не было. Поэтому выбор пал на Openfire от компании Igniterealtime, кстати, автора одного из самых популярных XMPP-клиентов для Android – Smack.
Детали нашей реализации
XMPP сервер – Openfire https://www.igniterealtime.org/projects/openfire/.
Клиент для фронтэнда – Strophe.js https://github.com/strophe/strophejs.
Клиент для Java сервисов и Android – Smack https://www.igniterealtime.org/projects/smack/.
Интеграция с хранилищем пользователей и системой авторизации – реализована через плагины Openfire.
Для оптимизации работы с нашим веб-приложением на PHP реализовали отправку сообщений через плагин с REST API – иначе каждый раз авторизовываться получается накладно по времени и ресурсам. Дополнительная фишка плагина – поддерживается отправка сообщений в json, включая наши дополнительные поля:
{
"from": "Отправитель",
"to": "Получатель",
"headers": {
"urgency": {
"@xmlns": "http://rtmis.ru/protocol/xmpp/common",
"value": 3
},
"disease": {
"@xmlns": "http://rtmis.ru/protocol/xmpp/disease",
"diag": {
"@code": "X57",
"value": "Лишения неуточненные"
},
"phase": {
"@id": 1,
"value": "Ранняя"
}
}
},
"body": "Пациент находится в приемном отделении"
}
Кроме того, сообщения в этом плагине складываются в очередь – дополнительный плюс для масштабируемости.
Уведомления мы сделали через комнаты (групповой чат) – бот отправляет сообщения в нужную комнату и все, кто в нее входит, получают сообщения. Обычно комнаты создаются по принципу «одна комната – одно отделение». Это оказался самый быстрый и простой способ для реализации.
Общие впечатления по XMPP и Openfire
XML-природа протокола пусть избыточна, но строга и удобна.
XEP описывают много «вкусных» вещей, но надо внимательно смотреть, что реализовано для конкретных клиента и сервера. Список для Openfire: http://download.igniterealtime.org/openfire/docs/latest/documentation/protocol-support.html. Для нас, в целом, этот список оказался достаточным.
Понятие «ресурса» («устройства») – может быть применено очень широко, например, у нас в качестве «устройства» может выступать боковая панель уведомлений для веб-приложения, основное окно чата в том же веб-приложении, мобильное устройство, приложение – «пейджер» и т.д.
К достоинствам Openfire можно отнести:
активную разработку;
много готовых плагинов https://www.igniterealtime.org/projects/openfire/plugins.jsp;
хорошие возможности для кастомизации: с помощью плагинов и расширений можно настроить авторизацию, обработку пакетов, маршрутизацию и многое другое.
Из недостатков:
не очень удачно реализован механизм плагинов, реализующих собственное REST API. По всей видимости, авторы изначально не особо рассчитывали на такое применение, поэтому получилось то, что получилось;
отсутствует автоматическая чистка истории в комнатах – удаляем скриптом из БД;
при старте Openfire подгружается ВСЯ история по ВСЕМ комнатам – приводит к резкому росту потребления памяти, решилось ограничением глубины истории;
по умолчанию неиспользуемые комнаты удаляются. Долго искали в чем причина, пока не нашли что это регулируется опцией «Disable MUC room unloading for this service» в свойствах службы группового чата. Здесь же можно настроить после скольких дней неиспользуемая комната будет удалена, а также загружать или нет все комнаты при старте.
Кроме этого, для нас определенной проблемой стало развертывание сервиса на регионах – первоначальные варианты и особенности инфраструктуры требовали применения ручных настроек, и, к сожалению, человеческий фактор дал о себе знать. В последствии настройки и контейнеры доработали, стало гораздо проще.
Заключение
Если нужно быстро поднять корпоративный централизованный мессенджер или интегрировать его в существующий продукт – XMPP и Openfire отличный вариант для старта. Чат, групповой чат – все работает «из коробки».
Нужно внимательно смотреть какие XEP реализует используемый сервер и клиент.
В целом, XMPP – это ближе к фреймворку, когда сервер (и клиент) реализуют много всяких интересных штук, но требуется приложить определенное усилие для того, чтобы это стало законченным решением.
Перспективы (планы на следующий этап):
переход на использование PubSub вместо группового чата для уведомлений;
«Пейджер» и push-уведомления для врачей – с отправкой обезличенных данных по незащищенным сетям;
авторизация через Госуслуги;
интеграция Openfire с Jitsi (https://jitsi.org/) для аудио-видео конференций;
интеграция Openfire с Minio/IPFS для хранения больших файлов, в том числе записей конференций.