Вводные:
Вариант решения:
Организация WebSocket сервера:
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
Related Posts
 Получение логина и пароля техподдержки 1С из базы Получение логина и пароля техподдержки 1С из базы
 Класс для вывода отчета в Excel Класс для вывода отчета в Excel
 Счет-фактура для УПП Счет-фактура для УПП
 Библиотека классов для создания внешней компоненты 1С на C# Библиотека классов для создания внешней компоненты 1С на C#
 Акт об оказании услуг (со скидками) — внешняя печатная форма для Управление торговлей 11.1.10.86 Акт об оказании услуг (со скидками) — внешняя печатная форма для Управление торговлей 11.1.10.86
 Прайс-лист с артикулом в отдельной колонке Прайс-лист с артикулом в отдельной колонке
 
    

*) Для обертки приложения в службу можно использовать nssm
*) Условно-бесплатная компонента для веб-гнезд с поддержкой «Внешнее Событие»
Про «поднимать веб-гнезда на сервере сложно» согласен. Через костыли.
Что бы экземпляр компоненты жил в потоке на сервере этот самый поток надо создать.
Как вариант, написать фоновое задание, которое будет создавать поток.
Этот поток передать во внешнюю компоненту вызовом метода и удерживать его там, передавая управление 1С только при получении сообщения.
Пока Истина Цикл
Сообщение = ВнешняяКомпонента.ПолучитьСообщение() // Здесь поток замирает до получения сообщения
ОбработкаСообщения(Сообщение);
КонецЦикла
Для этого можно использовать
(2) В моем случае нет смысла,
1. если что-то нужно передать на сервер, то вызов серверного метода,
2. если нужно отправить сообщение другому пользователю то организовать канал public и в него писать.
В Публикации 937068 нет описания функций. Загонять в бесконечный цикл с ПолучитьСообщение() не самое лучшее решение. В той что использую используется нормальный механизм.
У нас есть компонента CentrifugoClient (основана на WebSocketClient), которая уже заточена под Centrifugo. Используем её на предприятии уже больше года. Скоро опубликую.
В CentrifugoClient реализованы методы: ‘Connect’, ‘Refresh’, ‘Disconnect’, ‘Subscribe’, ‘Unsubscribe’, ‘Publish’, ‘Presence’, ‘History’, ‘Ping’, ‘CreateToken’
Пример обмена CentrifugoClient и Android будет рассмотрен в публикации.
Вообще Centrifugo работает под Windows, по крайней мере запускается и пускает в административный веб-интерфейс:
C:Usersadmin>centrifugo.exe —admin {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»starting Centrifugo 2.2.2 (go1.12.6)»} {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»config path: C:\Users\admin\config.json»} {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»pid: 12684″} {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»engine: Memory»} {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»gomaxprocs: 4″} {«level»:»info»,»time»:»2019-09-27T11:50:44+03:00″,»message»:»serving websocket, SockJS, API, admin endpoints on :8000″} {«level»:»info»,»time»:»2019-09-27T11:53:11+03:00″,»message»:»signal received: interrupt»} {«level»:»info»,»time»:»2019-09-27T11:53:11+03:00″,»message»:»shutting down, wait…»}Показать
(6) В ручную запустить возможно, но настроить запуск как «Службу» (чтобы запускалась при запуске windows без необходимости запускать сеанс пользователя) у меня не получилось. Пробовал . Для меня не критично, потому что планирую развернуть на отдельной виртуальной машине с ubuntu.
(6) Описание запуска в качестве сервиса windows добавил в статью.