К сожалению, «1С:Предприятие 8» не имеет возможности отслеживать внешние события, которые не связаны с сеансом пользователя или фонового задания. Статья поможет обойти этот недостаток с помощью c# прокси сервиса и HTTP-сервиса на стороне «1C:Предприятие 8». Взаимодействие между системами полностью соответствует рисунку.
Считаем, что RabbitMQ установлен и настроены очереди с обменами. События приходят (на рис. publish) от внешних источников данных в обмены (на рис. exchange) и с помощью адресации (на рис. routes) попадают в необходимые очереди (на рис. queue). Прокси сервис подписывается на событие появления нового сообщения в очереди или очередях. При появлении нового сообщения в очереди, прокси сервис получает его (на рис. consumes) и передает по протоколу HTTP в «1C:Предприятие 8». Обработка ошибок, повторная пересылка сообщений, логирование, rpc, crud и прочие вещи для рабочих систем расматриваться в данной статье не будут. Основная цель рассмотреть создание прокси сервиса на языке c# для Windows систем.
Создание проекта Windows c# сервиса
Создаем проект из шаблона как показано на рисунке: Установим все необходимые зависимости из NuGet консоли, которые потребуются в этом проекте:
- IoC-контейнер Unity, команда: install-package Unity
- библиотеку для работы с RabbitMQ, команда: install-package RabbitMQ.Client
В контекстном меню дизайнера формы (Service1.cs [Design]) выбираем пункт Properties: Установим свойство ServiceName: ProxyService. Переименуем файл проекта Service1.cs в ProxyService.cs.
Проект решения сервиса полностью создан. Можно переходить к созданию функционального кода.
Создание функционального кода сервиса
Создадим интерфейс, который будет предназначен для описания процесса, подписки на события очередей при запуске сервиса и отписки при остановке сервиса. Интерфейс позволит переключаться между различными реализациями (различными поставщиками очередей) при необходимости.
namespace ProxyService.Services { public interface IQueueService : IDisposable { void Subscribe(); void UnSubscribe(); } }
Для чтения настроек из файла конфигурации напишем небольшой хелпер, а так же добавим зависимость в проект System.configuration.
using System.ComponentModel; using System.Configuration; namespace ProxyService { public static class AppSettings { public static T Get(string key) { var appSetting = ConfigurationManager.AppSettings[key]; if (string.IsNullOrWhiteSpace(appSetting)) throw new System.Exception(key); var converter = TypeDescriptor.GetConverter(typeof(T)); return (T)(converter.ConvertFromInvariantString(appSetting)); } } }
Создадим реализацию интерфейса IQueueService.
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using RabbitMQ.Client; using RabbitMQ.Client.Events; namespace ProxyService.Services { public class RabbitMQQueueService : IQueueService { private bool disposed = false; private IModel _channel; private String _consumerTag; private IConnection _connection; private readonly IConnectionFactory _factory; private string WorkQueue { get; set; } private string RequestUri { get; set; } private AuthenticationHeaderValue authenticationHeader; public RabbitMQQueueService(IConnectionFactory factory) { this._factory = factory; this._connection = _factory.CreateConnection(); this._channel = _connection.CreateModel(); InitializeService(); } private void InitializeService() { WorkQueue = AppSettings.Get<string>("WorkQueue"); RequestUri = AppSettings.Get<string>("RequestUri"); authenticationHeader = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String( Encoding.ASCII.GetBytes( string.Format("{0}:{1}", AppSettings.Get<string>("UserName"), AppSettings.Get<string>("Password"))))); } public void Subscribe() { var consumer = new EventingBasicConsumer(_channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); using (HttpClient client = new HttpClient()) { client.DefaultRequestHeaders.Authorization = authenticationHeader; try { using (HttpResponseMessage response = client.PostAsync(RequestUri, new StringContent(message)).Result) { if (!response.IsSuccessStatusCode) { // Log error } } } catch (Exception e) { // Log exception } } }; _consumerTag = _channel.BasicConsume(queue: WorkQueue, noAck: true, consumer: consumer); } public void UnSubscribe() { _channel.BasicCancel(_consumerTag); } ~RabbitMQQueueService() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (this.disposed) { return; } if (disposing) { _channel?.Dispose(); _connection?.Dispose(); } disposed = true; } } }
При запуске сервиса Unity прочитает настройки конфигурационного файла и создаст необходимый IConnectionFactory, в конструкторе происходит инициализация подключения и канала, а так же устанавливаются параметры доступа к HTTP-сервису «1C:Предприятие 8». При старте сервиса выполнялся подписка на события очереди WorkQueue, при появлении сообщения оно будет отправлено в «1C:Предприятие 8».
Так как нет возможности с помощью Unity инициализировать поля класса ConnectionFactory при его создании, выполним небольшой трюк для обхода этого ограничения. Трюк заключается в использовании наследования.
using System; using System.Collections.Generic; using System.Reflection; using RabbitMQ.Client; namespace ProxyService.Services { public class RabbitMQConnectionFactory : ConnectionFactory { public RabbitMQConnectionFactory(Dictionary<string, String> fields) : base() { Type type = base.GetType(); FieldInfo[] fieldsInfo = type.GetFields(); foreach (var field in fields) { FieldInfo result = Array.Find(fieldsInfo, fi => fi.Name == field.Key); if (result != null) { string sType = result.FieldType.FullName; result.SetValue(this, Convert.ChangeType(field.Value, Type.GetType(sType))); } } } } }
При чтении настроек из конфигурационного файла, Unity создаст RabbitMQConnectionFactory и проинициализирует необходимые поля базового класса (в этом примере HostName), поля хранятся в Dictionary fields, этот объект так же соберет Unity. Посмотрим как должна выглядеть точка входа в сервис (Program.cs).
using System.ServiceProcess; using Microsoft.Practices.Unity; using Microsoft.Practices.Unity.Configuration; namespace ProxyService { static class Program { static void Main() { IUnityContainer container = new UnityContainer(); container.LoadConfiguration(); ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { container.Resolve() }; ServiceBase.Run(ServicesToRun); } } }
Происходит инициализация IoC-контейнера, далее выполняется загрузка конфигурации из конфигурационного файла, создается экземпляр сервиса для запуска, который будет построен с помощью Unity. Процесс создания экземпляра будет выглядеть так:
- Unity начнет разрешать зависимость класса ProxyService, конструктор которого принимает IQueueService, этот тип известен Unity из конфигурационного файла;
- согласно конфигурационному файлу, Unity будет создавать экземпляр класса RabbitMQQueueService, в которого так же есть своя зависимость IConnectionFactory;
- согласно конфигурационному файлу, Unity будет создавать экземпляр класса RabbitMQConnectionFactory, который принимает параметром конструктора Dictionary;
- Unity начнет сборку по цепочке обратно начиная с экземпляра класса Dictionary с заданными значениями из конфигурационного файла;
- После обратной сборки с помощью Unity на выходе получим экземпляр сервиса.
После всех этих манипуляций класс ProxyService будет выглядеть очень просто:
using System.ServiceProcess; using ProxyService.Services; namespace ProxyService { public partial class ProxyService : ServiceBase { private readonly IQueueService _queueService; public ProxyService(IQueueService queueService) { this._queueService = queueService; InitializeComponent(); } protected override void OnStart(string[] args) { _queueService.Subscribe(); } protected override void OnStop() { _queueService.UnSubscribe(); } } }
ProxyService.cs
namespace ProxyService { partial class ProxyService { private System.ComponentModel.IContainer components = null; protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } if (disposing) { _queueService?.Dispose(); } base.Dispose(disposing); } private void InitializeComponent() { components = new System.ComponentModel.Container(); this.ServiceName = "ProxyService"; } } }
ProxyService.Designer.cs
Конфигурационный файл (App.config) будет такого содержания:
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> <add key="WorkQueue" value="1c-work-queue" /> <add key="UserName" value="1c_admin" /> <add key="Password" value="1c_password" /> <add key="RequestUri" value="http://hostname/1cbase/hs/queue_app/query" /> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <alias alias="ISettings" type="System.Collections.Generic.IDictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib"/> <alias alias="Settings" type="System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib"/> <alias alias="IConnectionFactory" type="RabbitMQ.Client.IConnectionFactory, RabbitMQ.Client" /> <alias alias="RabbitMQConnectionFactory" type="ProxyService.Services.RabbitMQConnectionFactory, ProxyService" /> <alias alias="IQueueService" type="ProxyService.Services.IQueueService, ProxyService" /> <alias alias="RabbitMQQueueService" type="ProxyService.Services.RabbitMQQueueService, ProxyService" /> <register type="ISettings" mapTo="Settings" name="connectionSettings"> <method name="Add"> <param name="key" type="System.String" value="HostName" /> <param name="value" type="System.String" value="rabbitmq.local" /> <!-- --> <register type="IConnectionFactory" mapTo="RabbitMQConnectionFactory" name="connectionFactory"> <param name="fields"> <dependency name="connectionSettings" /> <property name="UserName" value="rabbitmq_user"/> <property name="Password" value="rabbitmq_password"/> <register type="IQueueService" mapTo="RabbitMQQueueService"> <param name="factory"> <dependency name="connectionFactory" />
На этом сервис полностью готов, необходимо указать верные данные для доступа к сервису RabbitMQ иначе прокси сервис будет вываливаться с ошибкой.
Создание установщиков для c# сервиса
Если ваша среда разработки не содержит шаблонов «Installer» их можно скачать по ссылке.
Дополнительные замечания
- Рассмотренный пример можно скачать по ссылке GitHub;
- пример не предназначен для промышленной эксплуатации, по причинах:
- последовательная обработка событий;
- нет обработки ошибок;
- доставка данных не гарантируется;
- не поддерживается повторная отправка сообщений;
- не реализован механизм remote procedure call;
- множество других нюансов, которые не есть темой этой статьи.
- installer не до конца настроен, при установки сервиса вы могли это заметить;
- конфигурационный файл храниться вместе с исполняемым файлом, примерный путь C:\Windows\SysWOW64;
- реализация HTTP-сервиса в «1С:Предприятие 8» упущена так, как не является темой статьи.
P.S. Примерно вот так выглядит система, которая проходит обкатку на рабочей базе:
Скомпилировал, настроил конфигурационный файл, запускаю службу и получаю:
Служба была запущена затем остановлена. Некоторые службы автоматически останавливаются, если они не используются другими службами или программами.
В чем может быть причина?
Посмотрите в сторону нового сервиса https://github.com/FoxyLinkIO/FoxyLink.RabbitMQ
там нету этой проблемы и разворачивается одной командой из консоли.
В текущем сервисе могут быть проблемы с Dependency Injection (вероятнее всего ошибка в конфигурационном файле).
Здравствуйте! Все настроила, запустила службу, все работает. Появился consumer на http://localhost:15672. Добавляю сообщение в очередь и сервис ничего не делает. При нажатии на сайте http://localhost:15672 «Get messages» говорит, что очередь пустая, хотя я написала свой reciever, который нормально получает сообщения. Подскажите п-та в чем может быть причина. Заранее спасибо)
Доброго времени суток, сервис отправляет сообщение по указанному адресу HTTP-сервиса и сообщение необходимо обработать на стороне 1С:Предприятия.
Установил FoxyLink. Установил FoxyLink.RabbitMQ Service, Настроил конфигурационный файл. При запуске службы выдает ошибку:
0хffffffff:0хffffffff
ОS: Windows seven x64
dotnet sdk 2.1.301 x64.
Вылетать не должен. Для себя понял, что нужно сделать Docker контейнер для того, чтобы не зависеть от окружения.
в секции
«AppEndpoints»: [
{
«Name»: «yt11»,
«Login»: «login»,
«Password»: «password»,
«Schema»: «http»,
«ServerName»: «1cweb.ktc.local:9292»,
«PathOnServer»: «yt11/hs/AppEndpoint/v1»
},
нужно заполнить настройки доступа к 1с web сервису?
а если его пока нет — служба будет вылетать с ошибкой?
Разобрался. нужно после изменения конфигурации перебилдить сервис. Служба запустилась и появилась в рабите !
Служба запустилась. В Раббите висит слушатель, но сообщения он не принимает. я пробовал отправлять вручную, сообщения становятся в очередь и все на этом.
Может чтото еще надо настроить? Документации нет!
Для каждого сообщения задайте свойства:
content_encoding : например, utf-8
content_type: например, application/json
type: например, yt11.FactShopTemperature.create.async (где yt11 параметр из конфигурационного файла)
Правильно составить сообщение вроде удалось. Теперь сервис помещает сообщение в очередь 1c.foxylink.invalid с ошибкой:
{«result»:»failed»,»log»:»System.Net.Http.HttpRequestException: Попытка установить соединение была безуспешной,
т.к. от другого компьютера за требуемое время не получен нужный отклик, или было разорвано уже установленное соединение из-за неверного отклика уже подключенного компьютера.
Подозреваю не правильно заполнил секцию AppEndPoints:
«AppEndpoints»: [
{
«Name»: «FL»,
«Login»: «»,
«Password»: «»,
«Schema»: «http»,
«ServerName»: «192.168.61.155»,
«PathOnServer»: «FL/hs/AppEndpoint/v1»
}
У меня одна точка и тут ошибиться трудно.
Прошел все подводные камни — обмен заработал. В процессе тестирования обнаружил нехорошую ситуацию. Сообщение попадает в очередь, слушатель забирает его и пытается переправить адресату и если на этом этапе возникает ошибка, например http-сервер упал или еще что, он возвращает в инвалидную очередь сообщение об ошибке, а само сообщение исчезает. Получается сообщения пропадают. Прошу прокомментировать как гарантировать доставку сообщений без потерь.
Да эта проблема известна, сегодня планирую выкатить обновление.
Выкатил обновление, все вопросы лучше обсуждать по ссылке: https://github.com/FoxyLinkIO/FoxyLink.RabbitMQ/issues
[…] Создание Windows C# сервиса для оповещения … […]