Вводные:
1. Используем уведомления пользователей о входящих звонках.
2. Транспортом сообщений является "1С Сервер взаимодействия". В связи с лицензионной политикой дальнейшее использование невозможно.
Вариант решения:
1. Использование WebSocket соединений на клиентской части.
2. Использование HTTP API интерфейса на серверной. Поднимать WebSocket соединение на сервере неудобно.
Организация WebSocket сервера:
Запустить как сервис для windows не получилось, не передает код ответа и сервис не стартует.
Планирую запускать на виртуальной nix, дополнительно можно настроить безопасность с помощью NGINX.
Для запуска нужно
1. Распаковать zip архив
2. Запустить генерацию типового конфигурационного файла
centrifugo genconfig
3. Запустить сервис
centrifugo --config=config.json
Сервис запустится на localhost:8000
Реализация подключения клиента
Использую NatieAPI компоненту.
Компоненту приобрел у //infostart.ru/public/1112969/, умеет генерировать внешнее событие.
Подключение компоненты реализовал в общем модуле, сделал обращения к нему из МодуляПриложения
Процедура ПодключитьКомпоненту(ПопыткаУстановки = Ложь) Экспорт
НастройкиПодключения = centrifuge_ВызовСервераПовторноеИспользование.ПолучитьНастройкиПодключения();
Если Не НастройкиКорректны(НастройкиПодключения) Тогда
Возврат;
КонецЕсли;
ДополнительныеПараметры = Новый Структура;
ДополнительныеПараметры.Вставить("ПопыткаУстановки", ПопыткаУстановки);
ДополнительныеПараметры.Вставить("НастройкиПодключения", НастройкиПодключения);
ОписаниеОповещения = Новый ОписаниеОповещения("НачатьПодключениеВнешнейКомпонентыЗавершение", ЭтотОбъект, ДополнительныеПараметры);
НачатьПодключениеВнешнейКомпоненты(ОписаниеОповещения, "ОбщийМакет.WebSocketClient", "SD3", ТипВнешнейКомпоненты.Native);
КонецПроцедуры
Процедура НачатьУстановкуВнешнейКомпонентыЗавершение(Результат) Экспорт
ПодключитьКомпоненту(Истина);
КонецПроцедуры
Процедура НачатьПодключениеВнешнейКомпонентыЗавершение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Истина Тогда
КомпонентаWebSocketClient = Новый("AddIn.SD3.WebSocketClient_ASync");
КомпонентаWebSocketClient.Открыть(ДополнительныеПараметры.НастройкиПодключения.ИмяСервера);
ИначеЕсли ДополнительныеПараметры.ПопыткаУстановки = Ложь Тогда
ОписаниеОповещения = Новый ОписаниеОповещения("НачатьУстановкуВнешнейКомпонентыЗавершение", ЭтотОбъект);
НачатьУстановкуВнешнейКомпоненты(ОписаниеОповещения, "ОбщийМакет.WebSocketClient");
Иначе
centrifuge_ВызовСервераПовторноеИспользование.ЗаписатьСостояниеПодключения("ОшибкаЗагрузкиКомпоненты");
КонецЕсли;
КонецПроцедуры
Далее анализирую сообщения от внешней компоненты
Анализ сообщений от компоненты
Процедура ОбработкаВнешнегоСобытия(Источник, Событие, Данные) Экспорт
НастройкиПодключения = centrifuge_ВызовСервераПовторноеИспользование.ПолучитьНастройкиПодключения();
Если Не НастройкиКорректны(НастройкиПодключения) Тогда
Возврат;
КонецЕсли;
Если Лев(Источник,16)="WebSocketClient_" Тогда
Если Событие = "Ping" Тогда
Возврат;
ИначеЕсли Событие = "Open" Тогда
ПодключитсяКСерверу(НастройкиПодключения.ИДПользователя, НастройкиПодключения.Секрет);
ИначеЕсли Событие="Message" Тогда
СтрокаДанные = КомпонентаWebSocketClient.ПолучитьСообщениеКакСтроку();
СоответствиеДанные = centrifuge_ВызовСервера.ИЗ_JSON(СтрокаДанные);
Если СоответствиеДанные.Получить("id") = 1
И Не ПустаяСтрока(СоответствиеДанные.Получить("result").Получить("client")) Тогда
centrifuge_ВызовСервераПовторноеИспользование.ЗаписатьСостояниеПодключения("ПодключенКСерверу");
ПодключитсяККаналу(НастройкиПодключения.ИДПользователя);
ИначеЕсли СоответствиеДанные.Получить("id") = 2
И ТипЗнч(СоответствиеДанные.Получить("result")) = Тип("Соответствие")
И СоответствиеДанные.Получить("result").Количество() = 0 Тогда
centrifuge_ВызовСервераПовторноеИспользование.ЗаписатьСостояниеПодключения("ПодключенККаналу");
Иначе
Попытка
ОбработкаДанныхВходящегоСообщения(СоответствиеДанные.Получить("result").Получить("data").Получить("data"));
Исключение
//ОписаниеОшибки()
КонецПопытки;
КонецЕсли;
ИначеЕсли Событие="Close" Тогда
centrifuge_ВызовСервераПовторноеИспользование.ЗаписатьСостояниеПодключения("Отключен");
КонецЕсли;
//Сообщить("WebSocketClient событие, Источник=" + Источник + ", Событие=" + Событие + ", Данные=" + Данные);
КонецЕсли;
КонецПроцедуры
И создаю подключение
Создание подключения и подписка на канал
Процедура ПодключитсяКСерверу(ИДПользователя, Секрет) Экспорт
Payload = Новый Структура();
Payload.Вставить("sub", ИДПользователя);
Токен = centrifuge_JWT.Encode(Секрет, Payload);
СоответствиеПараметры = Новый Соответствие;
СоответствиеПараметры.Вставить("token", Токен);
СооветствиеПодключение = Новый Соответствие;
СооветствиеПодключение.Вставить("id", 1);
СооветствиеПодключение.Вставить("method", "connect");
СооветствиеПодключение.Вставить("params", СоответствиеПараметры);
СтрокаДанные = centrifuge_ВызовСервера.В_JSON(СооветствиеПодключение);
КомпонентаWebSocketClient.ОтправитьСтроку(СтрокаДанные);
КонецПроцедуры
Процедура ПодключитсяККаналу(ИДПользователя) Экспорт
СоответствиеПараметры = Новый Соответствие;
СоответствиеПараметры.Вставить("channel", ИДПользователя);
СооветствиеПодключение = Новый Соответствие;
СооветствиеПодключение.Вставить("id", 2);
СооветствиеПодключение.Вставить("method", "subscribe");
СооветствиеПодключение.Вставить("params", СоответствиеПараметры);
СтрокаДанные = centrifuge_ВызовСервера.В_JSON(СооветствиеПодключение);
КомпонентаWebSocketClient.ОтправитьСтроку(СтрокаДанные);
КонецПроцедуры
Для Аутентификации используется JWT, реализацию брал из //infostart.ru/public/611505/, исходный код https://github.com/pintov/1c-jwt
В качестве имени канала использую GUID текущего пользователя, смысла слать broadcast запросы в моей задаче нет. Канал создается при подписке на него хотя бы одного клиента. То что сообщения не будут доставлены до клиента при отключении в данный не критично, доставляю оперативные уведомления.
Обрабатываю сообщения от компоненты на клиенте
Вариант обработки сообщений
Процедура ОбработкаДанныхВходящегоСообщения(СоответствиеСообщение)
ТипСообщения = СоответствиеСообщение.Получить("type");
ТекстСообщения = СоответствиеСообщение.Получить("text");
Таймаут = 10; //СоответствиеСообщение.Получить("timeout");
Заголовок = СоответствиеСообщение.Получить("header");
Картинка = Неопределено;
Если ТипСообщения = "Message" Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "" + ТекстСообщения;
Сообщение.Сообщить();
ИначеЕсли ТипСообщения = "MessageBox" Тогда
ОписаниеОповещенияОЗавершении = Новый ОписаниеОповещения("ПоказатьПредупреждениеЗавершение", ЭтотОбъект);
ПоказатьПредупреждение(ОписаниеОповещенияОЗавершении, ТекстСообщения, Таймаут, Заголовок);
ИначеЕсли ТипСообщения = "UserNotification" Тогда
ОписаниеОповещенияПриНажатии = Новый ОписаниеОповещения("ДействиеПриНажатииЗавершение", ЭтотОбъект);
ПоказатьОповещениеПользователя(Заголовок, ОписаниеОповещенияПриНажатии, ТекстСообщения, Картинка, СтатусОповещенияПользователя.Важное, Строка(Новый УникальныйИдентификатор()));
ИначеЕсли ТипСообщения = "OpenForm" Тогда
Параметры = Новый Структура;
Параметры.Вставить("Заголовок", Заголовок);
Параметры.Вставить("ТекстСообщения", ТекстСообщения);
ОткрытьФорму("ОбщаяФорма.centrifuge_ФормаУведомления", Параметры, , Строка(Новый УникальныйИдентификатор()));
КонецЕсли;
КонецПроцедуры
Реализация отправки сообщений с сервера
Для отправки использую HTTP протокол
Пример реализации отправки
&НаКлиенте
Процедура ОтправитьСообщение(Команда)
СоответствиеДанные = Новый Соответствие;
СоответствиеДанные.Вставить("text", ТекстСообщения);
СоответствиеДанные.Вставить("header", ЗаголовокСообщения);
СоответствиеДанные.Вставить("type", ТипСообщения);
СоответствиеПараметры = Новый Соответствие;
СоответствиеПараметры.Вставить("data", СоответствиеДанные);
СоответствиеПараметры.Вставить("channel", Строка(Пользователь.УникальныйИдентификатор()));
СоответствиеДанные = Новый Соответствие;
СоответствиеДанные.Вставить("method", "publish");
СоответствиеДанные.Вставить("params", СоответствиеПараметры);
centrifuge_ВызовСервера.ОтправитьСообщение(СоответствиеДанные);
КонецПроцедуры
Процедура ОтправитьСообщение(СоответствиеДанные) Экспорт
СтрокаДанные = В_JSON(СоответствиеДанные);
ДанныеОтвет = POST("/api", СтрокаДанные);
Если НЕ (ДанныеОтвет.КодСостояния = 200) Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Ошибка отправки";
Сообщение.Сообщить();
КонецЕсли;
КонецПроцедуры
При отправке используется Аутентификация по токену
Пример настройки HTTP запроса
Функция ПолучитьHTTPСтруктура(АдресРесурса)
НастройкиПодключения = centrifuge_ВызовСервераПовторноеИспользование.ПолучитьНастройкиПодключения();
HTTPСоединение = Новый HTTPСоединение(НастройкиПодключения.СерверAPI,НастройкиПодключения.ПортAPI,,,,,);
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-type", "application/json");
Заголовки.Вставить("Authorization", "apikey " + НастройкиПодключения.КлючAPI);
HTTPЗапрос = Новый HTTPЗапрос(АдресРесурса, Заголовки);
HTTPСтруктура = Новый Структура;
HTTPСтруктура.Вставить("HTTPСоединение", HTTPСоединение);
HTTPСтруктура.Вставить("HTTPЗапрос", HTTPЗапрос);
Возврат HTTPСтруктура;
КонецФункции
В результате получаем:
Реализовывал в виде расширения в которое включена компонента, поэтому все расширение не публикую.
Проверка подключений и восстановление:
Реализовано через ОбработчикОжидания, он обращается к глобальному клиентскому модулю который не видит переменную объявленную в модуле приложения, поэтому вызов передается в не глобальный клиентский общий модуль.
Процедура НачатьПодключениеВнешнейКомпонентыЗавершение(Результат, ДополнительныеПараметры) Экспорт
Если Результат = Истина Тогда
КомпонентаWebSocketClient = Новый("AddIn.SD3.WebSocketClient_ASync");
КомпонентаWebSocketClient.Открыть(ДополнительныеПараметры.НастройкиПодключения.ИмяСервера);
ПодключитьОбработчикОжидания("ПроверитьПодключение", 120);
Реализация проверки на клиенте
Процедура ПроверитьПодключениеКлиент() Экспорт
Если ph_srv_centrifugo_Сервер.ПолучитьСотояниеПодключения() = ПредопределенноеЗначение("Перечисление.ph_srv_Centrifugo_СостоянияПодключений.Отключен") Тогда
Попытка
НастройкиПодключения = ph_srv_centrifugo_ВызовСервераПовторноеИспользование.ПолучитьНастройкиПодключения();
КомпонентаWebSocketClient.Открыть(НастройкиПодключения.ИмяСервера);
Исключение
//ОписаниеОшибки()
КонецПопытки;
КонецЕсли;
КонецПроцедуры
Состояния подключений фиксирую в РС "ph_srv_Centrifugo_СостоянияПодключений"
Запись и запрос данных из РС
Процедура ЗаписатьСостояниеПодключения(Состояние) Экспорт
МЗ = РегистрыСведений.ph_srv_Centrifugo_СостоянияПодключений.СоздатьМенеджерЗаписи();
МЗ.Пользователь = ПараметрыСеанса.ТекущийПользователь;
МЗ.Прочитать();
МЗ.Пользователь = ПараметрыСеанса.ТекущийПользователь;
МЗ.Состояние = Перечисления.ph_srv_Centrifugo_СостоянияПодключений[Состояние];
МЗ.Записать();
КонецПроцедуры
Функция ПолучитьСотояниеПодключения() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ph_srv_Centrifugo_СостоянияПодключений.Состояние КАК Состояние
|ИЗ
| РегистрСведений.ph_srv_Centrifugo_СостоянияПодключений КАК ph_srv_Centrifugo_СостоянияПодключений
|ГДЕ
| ph_srv_Centrifugo_СостоянияПодключений.Пользователь = &Пользователь";
Запрос.УстановитьПараметр("Пользователь", ПараметрыСеанса.ТекущийПользователь);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Состояние;
КонецЕсли;
Возврат Перечисления.ph_srv_Centrifugo_СостоянияПодключений.ПустаяСсылка();
КонецФункции
Установка и запуск как служба:
Windows: успешно создал службу из exe с помощью https://nssm.cc/, при использовании sc служба не запускалась.
Ubuntu 18.04:
1. Запускаем строку
curl -s https://packagecloud.io/install/repositories/FZambia/centrifugo/script.deb.sh | sudo bash
из https://packagecloud.io/FZambia/centrifugo/install, раздел Installation, это настроит подключение к репозиторию, далее запускаем
sudo apt install centrifugo
Проверяем что запустился сервис (конфиг генерируется при установке)
sudo service centrifugo status
*) Для обертки приложения в службу можно использовать nssmhttps://www.nssm.cc/
https://infostart.ru/public/808432/
*) Условно-бесплатная компонента для веб-гнезд с поддержкой «Внешнее Событие»
Про «поднимать веб-гнезда на сервере сложно» согласен. Через костыли.
Что бы экземпляр компоненты жил в потоке на сервере этот самый поток надо создать.
Как вариант, написать фоновое задание, которое будет создавать поток.
Этот поток передать во внешнюю компоненту вызовом метода и удерживать его там, передавая управление 1С только при получении сообщения.
Пока Истина Цикл
Сообщение = ВнешняяКомпонента.ПолучитьСообщение() // Здесь поток замирает до получения сообщения
ОбработкаСообщения(Сообщение);
КонецЦикла
Для этого можно использоватьhttps://infostart.ru/public/937068/
(2) В моем случае нет смысла,
1. если что-то нужно передать на сервер, то вызов серверного метода,
2. если нужно отправить сообщение другому пользователю то организовать канал public и в него писать.
В Публикации 937068 нет описания функций. Загонять в бесконечный цикл с ПолучитьСообщение() не самое лучшее решение. В той что использую используется нормальный механизм.
У нас есть компонента CentrifugoClient (основана на WebSocketClient), которая уже заточена под Centrifugo. Используем её на предприятии уже больше года. Скоро опубликую.
В CentrifugoClient реализованы методы: ‘Connect’, ‘Refresh’, ‘Disconnect’, ‘Subscribe’, ‘Unsubscribe’, ‘Publish’, ‘Presence’, ‘History’, ‘Ping’, ‘CreateToken’
Пример обмена CentrifugoClient и Android будет рассмотрен в публикации.
Вообще Centrifugo работает под Windows, по крайней мере запускается и пускает в административный веб-интерфейс:
Показать
(6) В ручную запустить возможно, но настроить запуск как «Службу» (чтобы запускалась при запуске windows без необходимости запускать сеанс пользователя) у меня не получилось. Пробовалhttps://support.microsoft.com/ru-ru/help/251192/how-to-create-a-windows-service-by-using-sc-exe . Для меня не критично, потому что планирую развернуть на отдельной виртуальной машине с ubuntu.
(6) Описание запуска в качестве сервиса windows добавил в статью.