Опубликовано 15 комментариев

Создание Windows C# сервиса для оповещения «1С:Предприятия 8» об событиях RabbitMQ

Интеграция RabbitMQ через прокси сервер с «1C:Предприятие»

К сожалению, «1С:Предприятие 8» не имеет возможности отслеживать внешние события, которые не связаны с сеансом пользователя или фонового задания. Статья поможет обойти этот недостаток с помощью c# прокси сервиса и HTTP-сервиса на стороне «1C:Предприятие 8». Взаимодействие между системами полностью соответствует рисунку.

Считаем, что RabbitMQ установлен и настроены очереди с обменами. События приходят (на рис. publish) от внешних источников данных в обмены (на рис. exchange) и с помощью адресации (на рис. routes) попадают в необходимые очереди (на рис. queue). Прокси сервис подписывается на событие появления нового сообщения в очереди или очередях. При появлении нового сообщения в очереди, прокси сервис получает его (на рис. consumes) и передает по протоколу HTTP в «1C:Предприятие 8». Обработка ошибок, повторная пересылка сообщений, логирование, rpc, crud и прочие вещи для рабочих систем расматриваться в данной статье не будут. Основная цель рассмотреть создание прокси сервиса на языке c# для Windows систем.

Создание проекта Windows c# сервиса

Создаем проект из шаблона как показано на рисунке: Creating service projectУстановим все необходимые зависимости из NuGet консоли, которые потребуются в этом проекте:

  • IoC-контейнер Unity,  команда: install-package Unity
  • библиотеку для работы с RabbitMQ, команда: install-package RabbitMQ.Client

В контекстном меню дизайнера формы (Service1.cs [Design]) выбираем пункт Properties:  Service nameУстановим свойство 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. Процесс создания экземпляра будет выглядеть так:

  1. Unity начнет разрешать зависимость класса ProxyService, конструктор которого принимает IQueueService, этот тип известен Unity из конфигурационного файла;
  2. согласно конфигурационному файлу, Unity будет создавать экземпляр класса RabbitMQQueueService, в которого так же есть своя зависимость IConnectionFactory;
  3. согласно конфигурационному файлу, Unity будет создавать экземпляр класса RabbitMQConnectionFactory, который принимает параметром конструктора Dictionary;
  4. Unity начнет сборку по цепочке обратно начиная с экземпляра класса Dictionary с заданными значениями из конфигурационного файла;
  5. После обратной сборки с помощью 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. Примерно вот так выглядит система, которая проходит обкатку на рабочей базе:

Схема интеграции систем через RabbitMQ с «1С:Предприятие 8»
Схема интеграции систем через RabbitMQ с «1С:Предприятие 8»

15 комментариев к “Создание Windows C# сервиса для оповещения «1С:Предприятия 8» об событиях RabbitMQ

  1. Скомпилировал, настроил конфигурационный файл, запускаю службу и получаю:
    Служба была запущена затем остановлена. Некоторые службы автоматически останавливаются, если они не используются другими службами или программами.
    В чем может быть причина?

    1. Посмотрите в сторону нового сервиса https://github.com/FoxyLinkIO/FoxyLink.RabbitMQ
      там нету этой проблемы и разворачивается одной командой из консоли.

      В текущем сервисе могут быть проблемы с Dependency Injection (вероятнее всего ошибка в конфигурационном файле).

  2. Здравствуйте! Все настроила, запустила службу, все работает. Появился consumer на http://localhost:15672. Добавляю сообщение в очередь и сервис ничего не делает. При нажатии на сайте http://localhost:15672 «Get messages» говорит, что очередь пустая, хотя я написала свой reciever, который нормально получает сообщения. Подскажите п-та в чем может быть причина. Заранее спасибо)

    1. Доброго времени суток, сервис отправляет сообщение по указанному адресу HTTP-сервиса и сообщение необходимо обработать на стороне 1С:Предприятия.

  3. Установил FoxyLink. Установил FoxyLink.RabbitMQ Service, Настроил конфигурационный файл. При запуске службы выдает ошибку:
    0хffffffff:0хffffffff
    ОS: Windows seven x64
    dotnet sdk 2.1.301 x64.

    1. Вылетать не должен. Для себя понял, что нужно сделать Docker контейнер для того, чтобы не зависеть от окружения.

  4. в секции
    «AppEndpoints»: [
    {
    «Name»: «yt11»,
    «Login»: «login»,
    «Password»: «password»,
    «Schema»: «http»,
    «ServerName»: «1cweb.ktc.local:9292»,
    «PathOnServer»: «yt11/hs/AppEndpoint/v1»
    },
    нужно заполнить настройки доступа к 1с web сервису?
    а если его пока нет — служба будет вылетать с ошибкой?

  5. Разобрался. нужно после изменения конфигурации перебилдить сервис. Служба запустилась и появилась в рабите !

  6. Служба запустилась. В Раббите висит слушатель, но сообщения он не принимает. я пробовал отправлять вручную, сообщения становятся в очередь и все на этом.
    Может чтото еще надо настроить? Документации нет!

    1. Для каждого сообщения задайте свойства:
      content_encoding : например, utf-8
      content_type: например, application/json
      type: например, yt11.FactShopTemperature.create.async (где yt11 параметр из конфигурационного файла)

  7. Правильно составить сообщение вроде удалось. Теперь сервис помещает сообщение в очередь 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»
    }
    У меня одна точка и тут ошибиться трудно.

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

    1. Да эта проблема известна, сегодня планирую выкатить обновление.

    2. Выкатил обновление, все вопросы лучше обсуждать по ссылке: https://github.com/FoxyLinkIO/FoxyLink.RabbitMQ/issues

  9. […] Создание Windows C# сервиса для оповещения … […]

Добавить комментарий