Native внешняя компонента для оповещения по UDP или TCP

Native внешняя компонента для оповещения по UDP или TCP, которая реализует возможность в 1С передавать сообщения с сервера на клиента. Исходный код компоненты также представлен.

   Стояла сложная задача: настроить возможность совместной работы нескольких пользователей в одном заказе, чтобы не было блокировок и изменения других пользователей подсвечивались.

   Поначалу пытался решить эту задачу через использование обработчиков ожидания. Но это решение не понравилось, операторы хотели сразу видеть изменения других пользователей, были также другие причины.

   Затем была изучена и протестирована типовая система взаимодействия. Эта система вполне подходила, но был жирный минус: она почему-то отказывалась работать на обычных формах. Так как работа шла в конфигурации УТ 10.3, то эта система также была отброшена. 

   Начался мучительный поиск внешней компоненты для передачи оповещений по UDP или TCP. Большинство найденных компонент работали только на платформе 1С win32, а у нас стояла платформа 64 бит. Подходящая компонента все же была найдена и протестирована. Но так как она не поддерживала передачу кириллицы, то также была отброшена. 

   В итоге решил написать свою компоненту, которую назвал MyLib, так как появилась идея использовать ее не только для udp или tcp оповещения, но по мере необходимости добавлять туда и другой функционал. Компонента написана при помощи Visual Studio 2013 на C++ по технологии Native. За основу был взят пример с диска ИТС, поэтому некоторые методы компоненты остались по наследству. Все свойства и методы можно писать как по русски, так и по английски.

   Свойства:

  1. PortTypeIsTCP (ПотоковыйТипПорта) — логический тип. Задает тип оповещения: по tcp или udp.
  2. LocalPort (Локальный порт) — целочисленный тип. Задает номер порта, который будет открыт для прослушивания.
  3. Status (Статус) — строковый тип. Служебное свойство, которое можно использовать для отладки.
  4. LogFile (ЛогФайл) — строковый тип. Здесь задается полный путь к текстовому файлу, в который компонента может писать логи. Это использовалось на этапе отладки, сейчас не применяется.

   Методы:

  1. ShowInStatusLine(<Текст>) (ПоказатьВСтрокеСтатуса) — этот метод достался по наследству. На 5 секунд выводит в строку статуса полученный Текст.
  2. ExternalEvent(<Источник>, <Событие>, <Данные>) (ВнешнееСобытие) — достался по наследству. Возвращает true — событие помещено в очередь, или false — очередь переполнена, обработка событий недоступна или неизвестная ошибка. Помещает событие в очередь, записывая источник события (Источник — тип Строка), наименование (Событие — тип Строка) и параметры события (Данные — тип Строка). При обработке события эти данные передаются процедуре ВнешнееСобытие(<Источник>, <Событие>, <Данные>). ExternalEvent доступен только на клиенте.
  3. LoadPicture(<ИмяФайла>) (ЗагрузитьКартинку) — достался по наследству. Загружает изображение из указанного файла и передает его в "1С:Предприятие".
  4. ShowMessageBox() (ПоказатьСообщение) — достался по наследству. Выводит сообщение о версии платформы.
  5. OpenPort() (ОткрытьПорт) — запускает прослушивание порта, который задан в свойстве LocalPort.
  6. ClosePort() (ЗакрытьПорт) — останавливает прослушивание порта LocalPort.
  7. NotifyPort(<Порт>, <Источник>, <Событие>, <Данные>) (ОповеститьПорт) — посылает оповещение клиенту, у которого открыт Порт. У клиента при этом срабатывает процедура ВнешнееСобытие(<Источник>, <Событие>, <Данные>). Источник, Событие, Данные — это параметры строкового типа, не более 1 Кб длины, т.е. максимум 1024 символа каждый. 
  8. Pause(<Миллисекунд>) (Пауза) — останавливает выполнение программы на некоторое время без нагрузки на процессор.
  9. Loopback(<ДвоичныеДанные>) (Петля) — достался по наследству. Метод принимает один аргумент типа ДвоичныеДанные и возвращает его копию.

   В моем случае пользователи 1С работают на терминальном сервере, т.е. IP всегда равен 127.0.0.1, поэтому не выводил IP как параметр в метод NotifyPort(), чтобы не усложнять.

   Работа компоненты тестировалась на платформах 1С: 8.3.12.1616 32бит, 8.3.13.1809 64бит, 8.3.15.1489 64бит. Операционные системы: Windows server 2008, Windows 8. В настоящее время проект по совместной работе пользователей в одном заказе полностью реализован и используется. 

Простой пример подключения и использования MyLib

  • Добавляем в конфигурацию общий макет:

В архив AddInNativeWin.zip включены 3 файла: AddInNativeWin32.dll, AddInNativeWin64.dll, MANIFEST.XML.

  • В модуле обычного приложения:

Объявляем глобальную переменную:

Перем MyLib Экспорт;

Находим процедуру ПриНачалеРаботыСистемы и вставляем туда код: 

 //Стартуем сервер взаимодействия
Если ПодключитьВнешнююКомпоненту("ОбщийМакет.MyLib","MyLib",AddInType.Native) Тогда
MyLib = Новый("AddIn.MyLib.CppNativeExtension");
MyLib.LocalPort = МодульВзаимодействия.ПолучитьЛокальныйПорт();
MyLib.PortTypeIsTCP = Истина;
MyLib.OpenPort();
Иначе
Предупреждение("Компонента MyLib не подключена!");
КонецЕсли;

В процедуре ПриЗавершенииРаботыСистемы вставляем код:

 //Останавливаем сервер взаимодействия
Если MyLib <> Неопределено Тогда
MyLib.ClosePort();
MyLib = Неопределено;
КонецЕсли;
  • Добавляем в конфигурацию общий модуль МодульВзаимодействия (в свойствах ставим всего одну галочку "Сервер"):

//Данный алгоритм присвоения номера порта пользователю корректен только в том случае, если предположить,
//что пользователи не будут открывать по несколько сеансов одной базы 1С:Предприятие под одним именем.
//В противном случае, придется разрабатывать более сложную систему назначения портов.
Функция ПолучитьЛокальныйПорт() Экспорт
ЛокальныйПорт = 1024;
ТекущийПользовательИБ = ПользователиИнформационнойБазы.ТекущийПользователь();
Для Каждого Соединение Из ПолучитьСоединенияИнформационнойБазы() Цикл
Если Соединение.ИмяПриложения <> "Designer" Тогда
Если Соединение.Пользователь.УникальныйИдентификатор = ТекущийПользовательИБ.УникальныйИдентификатор Тогда
ЛокальныйПорт = ЛокальныйПорт + Соединение.НомерСеанса;
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат ЛокальныйПорт;
КонецФункции

Функция ПолучитьСписокОткрытыхПортов() Экспорт
ОткрытыеПорты = Новый Массив;
ТекущийПользовательИБ = ПользователиИнформационнойБазы.ТекущийПользователь();
Для Каждого Соединение Из ПолучитьСоединенияИнформационнойБазы() Цикл
Если Соединение.ИмяПриложения <> "Designer" Тогда
Если Соединение.Пользователь.УникальныйИдентификатор <> ТекущийПользовательИБ.УникальныйИдентификатор Тогда
ОткрытыеПорты.Добавить(1024 + Соединение.НомерСеанса);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Возврат ОткрытыеПорты;
КонецФункции

Процедура ОповеститьВсех(Событие,Данные,Источник) Экспорт
Для Каждого Порт Из ПолучитьСписокОткрытыхПортов() Цикл
MyLib.NotifyPort(Порт,"MyLib.Port: " + MyLib.LocalPort,Событие,Данные);
КонецЦикла;
Оповестить(Событие,Данные,Источник); //На случай, если требуется оповещение самому себе
КонецПроцедуры
  • В форме документа ЗаказПокупателя:

Ищем обработчик ПослеЗаписи и прописываем там код:

 МодульВзаимодействия.ОповеститьВсех("OrderRecord",Строка(Ссылка.УникальныйИдентификатор()),ЭтаФорма);

Добавляем новую процедуру:

Процедура ОбработкаОповещенияВзаимодействия(Источник, Событие, Данные)
Если Событие = "OrderRecord" Тогда
ЭтотОбъект.Прочитать();
ЭтаФорма.Обновить();
ОбновлениеОтображения();
Предупреждение("Форма обновлена!");
КонецЕсли;
КонецПроцедуры

В обработчик ВнешнееСобытие вставляем код:

 Если Лев(Источник,5) = "MyLib" Тогда
ОбработкаОповещенияВзаимодействия(Источник, Событие, Данные);
Возврат;
КонецЕсли;

В обработчик ОбработкаОповещения прописываем:

 ОбработкаОповещенияВзаимодействия(Источник, ИмяСобытия, Параметр);

 

   Вот такой простенький пример, на базе которого можно построить свою систему оповещения о каких-то событиях. В будущем планирую, если получится, на базе MyLib разработать систему асинхронных вызовов. Возможно, найдутся и другие сферы применения.

   Исходный код MyLib также выкладываю. Возможно, кто-то захочет его использовать как базу для своей внешней компоненты.

   На этом пока все.

10 Comments

  1. acanta

    Правильно ли я понимаю, что пауза в вашей компоненте может быть использована как аналог модального вопроса ?

    Reply
  2. vdv2701

    Да, правильно. Если на клиенте включить паузу, то определенное время интерфейс пользователя будет заблокирован.

    Reply
  3. gorenski

    Хорошая штука, из минусов — разве что название MyLib не отражает сути ))

    Reply
  4. serpent

    А для работы на НЕ терминальном сервере? наверное не подойдет?

    Нужен IP в метод NotifyPort() ??

    Reply
  5. vdv2701

    Да, если работа с базой 1С идет по локальной сети, например, а не в терминале, то метод NotifyPort() нужно доработать. Исходники есть, доработки несложные, добавить параметр IP.

    Reply
  6. EVKash

    (5) Из минусов видится, что если вдруг порт занят, то мы увидим просто «Компонента MyLib не подключена!».

    Не очень хорошо делать жесткую привязку к сеансу.

    Кстати… что вернет MyLib.OpenPort() в таком случае? Ошибку?

    И открытые порты я бы куда-нибудь в РС записывал. При подключении — обновлять.

    Из пожеланий — доработать под разные IP…

    Reply
  7. vdv2701

    Этот механизм назначения портов около 3 недель на рабочей базе, пока проблем не было, буду решать по мере поступления. Обычно служебные порты не превышают 1024, поэтому вероятность пересечения с каким-то служебным портом очень мала. Остальные ограничения этого механизма описаны в статье.

    Да, если порт занят, то MyLib.OpenPort() в свойстве компоненты Status вернет номер ошибки, но в статье этот случай не рассматривается.

    Reply
  8. Cyberhawk

    (7)

    вероятность пересечения с каким-то служебным портом очень мала

    Учитывая, что:

    — номер сеанса в большинстве типовых инфобаз через доволно короткое время начинает измеряться сотнями, тысячами и десятками тысяч

    — кластер в типовом исполнении занимает порты 1540-1591

    — количество портов ограничено 2#k8SjZc9Dxk16 (максимальный номер, таким образом, равен 65535)

    , то конфликты и сюрпризы неизбежны.

    Каждый порт-кандидат рекомендуется запрашивать у самой ОС, тогда она гарантированно выдаст валидный и незанятый.

    Reply
  9. vdv2701

    Пока не сталкивался с такими сюрпризами. Этот механизм нормально работает уже более месяца. Возможно, если будут еще внедрения, и какие-то конфликты появятся, то буду что-то придумывать.

    Reply
  10. bavkyz

    Хорошая статья про Native внешняя компонента, я добавил параметр ip-адрес, как и писал автор — это не сложно, но мне потребовалось около 8 часов, т.к. я не знаю c++.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *