Как мы выбирали идеальный протокол для мессенджера в ЕЦП.МИС (Медицинской информационной системе)

28dcefcaa35416ad4a615e69a2a8bfc8.png

У нас в «РТ МИС» уже был мессенджер для ЕЦП.МИС. Ну, как «мессенджер» — некий самописный сервис на Node.js и хранением сообщений в БД для общения врачей и групповых уведомлений типа «Терапия! Тортики в ординаторской, успевайте».

В один прекрасный день мы собрались и решили, что все, хватит: нам нужен новый продукт, чтобы было «модно и молодежно» и еще куча функций в придачу. Дорабатывали тогда как раз модуль Стационара — здесь и приемное отделение, куда постоянно кого-то привозят и надо отправлять уведомления врачам, и большой поток информации по результатам анализов, какие-то показатели пациентов, консилиумы и вот это все. Да еще где-то впереди маячили доработки Поликлиники с телемедицинскими консультациями и уведомлениями по статусам талонов на прием.

Основные задачи для мессенджера

  • текстовый чат пользователь — пользователь, обычно — это врач — врач или врач — медсестра;

  • различные уведомления пользователей, например, врача о появлении результатов анализов или прибытии пациента в приемное отделение;

  • текстовый чат в контексте пациента или случая лечения, когда врач может задать какие-то уточняющие вопросы другим врачам с привязкой к случаю лечения или пациенту;

  • передача файлов в сообщениях;

  • работа с историей сообщений (поиск, просмотр, отметки о прочтении).

    В перспективе

  • различные сценарии уведомления, например, уведомление врачу при смене статуса талона на запись в поликлинику, рассылки по должностям, структурным подразделениями;

  • «пейджер» — push-уведомления врачу на его личное мобильное устройство в случае, если врач не подключен к внутренней сети;

  • аудио-видео конференции (консилиум);

  • телемедицинские консультации, в том числе при участии пациента, авторизованного через Госуслуги;

Основные пользователи системы сообщений на данном этапе — врачи и средний медперсонал. Пациенты тут никак не задействованы, за исключением в перспективе их участия в телемедицинских консультациях, авторизовавшись через Госуслуги.

Вперед, на поиски!

fe6d65d359d5febcf9daa42322795937.png

Когда речь заходит о каких-то системах сообщений, «олдфаги» вспоминают IRC и ICQ. Если надо «модно и молодежно» — речь заходит о Discord и Slack. Поклонники приватности берут Matrix. Все остальные используют Telegram. Вроде бы бери и пользуйся. Но для наших целей это все абсолютно неприменимо: медицинская информация, с которой работают врачи, — это такой забористый коктейль из персональных данных и медицинской тайны, что требует особых подходов, в частности:

  • практически вся работа идет в защищенном контуре, куда нет доступа посторонним сервисам;

  • персональные данные и другая чувствительная информация должна храниться и обрабатываться в РФ;

  • нежелательно использовать каких-то иностранных поставщиков.

В общем, все это резко ограничивает набор возможных решений.

А чего, собственно, хочется от мессенджера?

  • открытость — отсутствие vendor lock in, открытый код;

  • контроль — возможность развернуть self-hosted инсталляцию;

  • наличие реализаций (клиентов) под основные языки (платформы), используемые у нас: Java, PHP, Node.js, Python;

  • стабильность — технология должна пройти фазу «хайпа»;

  • развитие — технология не должна быть «мертвой»;

  • шифрование — хорошо, но не в первую очередь, вся работа идет в защищенном контуре;

  • интеграция — возможность встраивания в существующие системы, в частности, авторизация и список пользователей;

  • расширяемость — некий подход для создания расширений в протоколе/ПО без «глобальных костылей».

Где-то тут мы стали понимать, что Телеграм, Дискорд и прочий Слак нас не спасут. Нужно переходить в область Open Source. Задачу осложняло то, что нужно было интегрироваться с нашими существующими системами и сервисами, а еще требовалась передача различной специфики по случаю лечения (врач, срочность, тип диагноза и т.д.). Можно было бы придумать свой формат сообщений — обмениваться json-ами и передавать всю необходимую специфику в полях, но хотелось не терять возможности работы с какими-то «стандартными» клиентами без наших доработок для облегчения интеграции сторонних модулей.

Что делать? Продолжать пилить что-то свое или все же есть выход?

c3d752238a993dea698c031a0e491d55.png

Безусловно, одним из возможных путей было оставить все как есть и продолжать развивать собственное решение. Тем более что на тот момент уже кроме чатов была в каком-то виде реализована поддержка аудиоконференций. Альтернативным направлением поиска стал переход от готовых решений в область протоколов. 

Беглый поиск показал активное развитие различных децентрализованных протоколов, например, 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) — используется для обмена сообщениями между пользователями. Выглядит примерно так:
    Привет, как дела?

Ростер (список контактов)

Разбитый на группы список Jabber-адресов ваших собеседников (контактов). Хранится на сервере и передаётся клиенту по запросу. Сервер также обрабатывает запросы на добавление, удаление контакта из списка, а также смены группы для конкретного контакта.

XEP (расширения)

XMPP Extension Protocol — расширение протокола XMPP. Например, XEP-0045 — многопользовательский чат, XEP-0084 — поддержка аватарок пользователей, XEP-0107 — статус пользователя (user mood). XEP описывают как какие-то базовые вещи (XMPP Core), так и множество продвинутого и очень интересного функционала.

Вообще, расширения — одна из самых интересных особенностей XMPP, когда, используя кирпичики описанные выше (различные стансы), мы описываем необходимый нам функционал. При желании можно сделать и собственное расширение. В настоящий момент насчитывается порядка двух сотен действующих XEP.

Использования XMPP

Для всех этих случаев можно выделить одну картину (особенно характерную для больших порталов): быстрый старт сервисов, используя 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

6acaedc9d756e430a1a9c1999045db2e.png

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 для хранения больших файлов, в том числе записей конференций.

© Habrahabr.ru