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

Petro Bazeliuk —  Апрель 10, 2016 — 14 комментариев
Интеграция RabbitMQ через прокси сервер с 1C:Предприятие

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

К сожалению, «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»

Petro Bazeliuk

Записи

Опыт работы с «1С:Предприятие 8» — более 10 лет, за это время реализовано 30 успешных проектов по итеративным методологиям Scrum и Kanban. Оптимальные решения для высоконагруженных ИБ с онлайном от 400 человек. Занимаюсь продвижением в массы системы контроля версий — git и методики git-flow, TDD, BDD, а также проработкой паттерна минимальной модификации конфигурации и внесением изменений без обновления базы данных. Время от времени участвую в проекте xUnitFor1C.

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

  1. 

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

    Нравится

  2. 

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

    Нравится

  3. 

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

    Нравится

  4. 

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

    Нравится

  5. 

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

    Нравится

  6. 

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

    Нравится

    • 

      Для каждого сообщения задайте свойства:
      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-сервер упал или еще что, он возвращает в инвалидную очередь сообщение об ошибке, а само сообщение исчезает. Получается сообщения пропадают. Прошу прокомментировать как гарантировать доставку сообщений без потерь.

    Нравится

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s