Windows Communication Foundation – программная платформа от Microsoft для создания, настройки и развертывания распределенных сетевых сервисов. WCF-runtime и его пространство имен System.ServiceModel, представляющее его главный программный интерфейс, это преемник технологий создания распределенных систем, успешно применяемых разработчиками для создания распределенных приложений на платформе Windows в последнее десятилетие. Разберём тестовый пример создания WCF-сервиса.

Открываем Visual Studio 2015 и создаём новый проект типа Class Library. Проект назовём WCFMyServiceLibrary.




Файл Class1.cs переименуем в MyService.cs и добавим ещё один класс, файл для которого назовём IMyService.cs.

Добавим ссылку на сборку System.ServiceModel.

В файле IMyService.cs опишем интерфейс:
using System.ServiceModel;

namespace WCFMyServiceLibrary
{

[ServiceContract]
public interface IMyService
  {
    [OperationContract]
    string Method1(string x);
    [OperationContract]
    string Method2(string x);
  }
}


В файле MyService.cs опишем реализацию интерфейса:
namespace WCFMyServiceLibrary
{
  public class MyService : IMyService
  {
    public string Method1(string x)
    {
      string s = $"1 You entered: {x} = = = 1";
      return s;
    }

    public string Method2(string x)
    {
      string s = $"2 you entered: {x} = = = 2";
      return s;
    }
  }
}

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

В том же решении (Solution) создадим новый проект типа «Служба Windows». Называем проект WindowsServiceHostForMyService.



Затем файл Service1.cs (только что созданного проекта) переименуем в MyService.cs. В этот проект добавим ссылку на сборку System.ServiceModel, а также не забываем указывать в файле MyService.cs директивы:

using System.ServiceModel;
using System.ServiceModel.Description;

В классе MyService добавляем новый член:

private ServiceHost service_host = null;

Также необходимо добавить ссылку на проект WCFMyServiceLibrary, который находится в этом же решении:



Затем в классе MyService изменим метод OnStart таким образом, чтобы в этом методе добавлялись конечные точки нашего сервиса (endpoint):

метод OnStart
    protected override void OnStart(string[] args)
    {
      if (service_host != null) service_host.Close();

      string address_HTTP = "http://localhost:9001/MyService";
      string address_TCP = "net.tcp://localhost:9002/MyService";

      Uri[] address_base = { new Uri(address_HTTP), new Uri(address_TCP) };
      service_host = new ServiceHost(typeof(WCFMyServiceLibrary.MyService), address_base);

      ServiceMetadataBehavior behavior = new ServiceMetadataBehavior();
      service_host.Description.Behaviors.Add(behavior);

      BasicHttpBinding binding_http = new BasicHttpBinding();
      service_host.AddServiceEndpoint(typeof(WCFMyServiceLibrary.IMyService), binding_http, address_HTTP);
      service_host.AddServiceEndpoint(typeof(IMetadataExchange),   MetadataExchangeBindings.CreateMexHttpBinding(), "mex");

      NetTcpBinding binding_tcp = new NetTcpBinding();
      binding_tcp.Security.Mode = SecurityMode.Transport;
      binding_tcp.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
      binding_tcp.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
      binding_tcp.Security.Transport.ProtectionLevel = System.Net.Security.ProtectionLevel.EncryptAndSign;
      service_host.AddServiceEndpoint(typeof(WCFMyServiceLibrary.IMyService), binding_tcp, address_TCP);
      service_host.AddServiceEndpoint(typeof(IMetadataExchange),  MetadataExchangeBindings.CreateMexTcpBinding(), "mex");

      service_host.Open();
    }

Затем реализуем остановку сервиса в методе OnStop:

    protected override void OnStop()
    {
      if (service_host != null)
      {
        service_host.Close();
        service_host = null;
      }
    }

Затем в Обозревателе решения — двойной клик на файле MyService.cs (проекта WindowsServiceHostForMyService) откроет этот файл в режиме конструктора (Design Mode).



На пустом пространстве вызываем контекстное меню (щелчок правой кнопкой мыши) и выбираем «Добавить установщик».



При этом будет создан новый класс ProjectInstaller.cs
Переименуем файл ProjectInstaller.cs в MyServiceInstaller.cs.
При этом выйдет окно с вопросом, следует ли переименовать зависимые объекты – отвечаем «Да».

Добавим в файл ссылку

using System.ServiceProcess;

Затем изменим код конструктора класса MyServiceInstaller:

    public MyServiceInstaller()
    {
      // InitializeComponent();
      serviceProcessInstaller1 = new ServiceProcessInstaller();
      serviceProcessInstaller1.Account = ServiceAccount.LocalSystem;
      serviceInstaller1 = new ServiceInstaller();
      serviceInstaller1.ServiceName = "WindowsServiceHostForMyService";
      serviceInstaller1.DisplayName = "WindowsServiceHostForMyService";
      serviceInstaller1.Description = "WCF Service Hosted by Windows NT Service";
      serviceInstaller1.StartType = ServiceStartMode.Automatic;
      Installers.Add(serviceProcessInstaller1);
      Installers.Add(serviceInstaller1);
    }

Заметим, что вызов метода InitializeComponent() мы заблокировали с помощью комментария.
На этом разработка службы Windows завершена. Собираем всё решение (Build Solution) и переходим к следующему этапу – установка службы Windows.

Для установки нашей службы создадим bat-файл (с произвольным названием, например Install_Windows_Service.bat) следующего содержания:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe WindowsServiceHostForMyService.exe

Нужно скопировать этот bat-файл в ту же папку, где находится скомпилированный файл WindowsServiceHostForMyService.exe (вам нужно заранее продумать, в какой папке будет лежать этот файл, который будет всегда запущен в качестве службы Windows).

Запускаем bat-файл, после чего наша программа WindowsServiceHostForMyService.exe будет установлена в качестве службы Windows.

Запустим эту службу с помощью стандартной программы управления службами.

Следующий этап – разработка клиентского приложения для использования предоставляемых сервисом услуг.

Для этого прежде всего нужно организовать так называемый «переходник» — Service Proxy – набор настроек, описывающих сервис для клиентского приложения.

Воспользуемся для этого стандартной утилитой SvcUtil.exe. Создадим файл Generate_Proxy.bat следующего содержания

SvcUtil  http://localhost:9001/MyService  /out:MyServiceProxy.cs  /config:App.config

Запустим этот файл (стандартная утилита SvcUtil.exe находится в папке C:\Program Files\Microsoft SDKs\Windows\v7.0\Bin).

Этот файл нужно запустить во время работы нашего сервиса, т.е. в данном случае, после успешного запуска службы Windows WindowsServiceHostForMyService.

В случае успешного запуска, программа SvcUtil.exe сгенерирует 2 файла — MyServiceProxy.cs и App.config.

Эти файлы необходимо добавить для клиентского приложения, чтобы это приложение могло вызывать методы нашей службы (чуть ниже вы узнаете, что файл App.config я решил не добавлять — обойдёмся и без него).

Примечание. Аналогичного результата можно было добиться, запустив

SvcUtil net.tcp://localhost:9002/MyService /out:MyServiceProxy.cs /config:App.config

Т.е. можно запускать эту утилиту, указав только одну конечную точку, либо http либо net.tcp.

В том же решении (Solution) создадим обычное приложение Windows Forms. Назовем его WindowsFormsApplication1



Добавим в этот проект ссылку на System.ServiceModel и, конечно же,

using System.ServiceModel в файле формы.

Добавим в этот проект файл MyServiceProxy.cs (именно его мы сгенерировали утилитой SvcUtil.exe). При этом следует добавить в файл MyServiceProxy.cs следующие строки:

namespace ServiceReference1
{
  using System.Runtime.Serialization;
  using System;
  … затем идёт содержимое файла MyServiceProxy.cs …
и конечно, не забываем поставить завершающую скобку для namespace
}

После этого, мы сможем ссылаться на класс MyServiceClient (этот класс создан программой SvcUtil.exe), указав в файле формы директиву.

 using ServiceReference1;

Пример готового файла MyServiceProxy.cs (комментарии удалены):
namespace ServiceReference1
{
  using System.Runtime.Serialization;
  using System;

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IMyService")]
public interface IMyService
{
    
 [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IMyService/Method1", ReplyAction="http://tempuri.org/IMyService/Method1Response")]
    string Method1(string x);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IMyService/Method2", ReplyAction="http://tempuri.org/IMyService/Method2Response")]
    string Method2(string x);
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface IMyServiceChannel : IMyService, System.ServiceModel.IClientChannel
{
}

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class MyServiceClient : System.ServiceModel.ClientBase<IMyService>, IMyService
{
    public MyServiceClient()
    {
    }
    
    public MyServiceClient(string endpointConfigurationName) : 
            base(endpointConfigurationName)
    {
    }
    
    public MyServiceClient(string endpointConfigurationName, string remoteAddress) : 
            base(endpointConfigurationName, remoteAddress)
    {
    }
    
    public MyServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(endpointConfigurationName, remoteAddress)
    {
    }
    
    public MyServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(binding, remoteAddress)
    {
    }
    
    public string Method1(string x)
    {
        return base.Channel.Method1(x);
    }
    
    public string Method2(string x)
    {
        return base.Channel.Method2(x);
    }
}
}


Поступим неординарно – и не будем добавлять файл App.Config в проект клиента!

Затем набросаем на форму 3 кнопки, текстовое поле textBox1 (пользователь вводит сюда строку для отправки на сервер) и поле richTextbox1 (имитация подсистемы сообщений нашего приложения – именно в это поле будут поступать сообщения от программы, в том числе и те, что вернул нам сервис)

Кнопка btn_Start – создаёт клиента
Кнопка btn_Send – отправляет сервису текстовую строку из текстового поля
Кнопка btn_Close – удаляет клиента из памяти и закрывает приложение

Код формы:
using System;
using System.ServiceModel;
using System.Windows.Forms;
using ServiceReference1;

namespace WindowsFormsApplication1
{

  public partial class Form1 : Form
  {

    MyServiceClient client = null;

    public Form1()
    {
      InitializeComponent();
    }

    private void Print(string text)
    {
      richTextBox1.Text += text + "\n\n";
      richTextBox1.SelectionStart = richTextBox1.Text.Length;
      richTextBox1.ScrollToCaret();
    }

    private void Print(Exception ex)
    {
      if (ex == null) return;
      Print(ex.Message);
      Print(ex.Source);
      Print(ex.StackTrace);
    }

    private void Create_New_Client()
    {
      if (client == null)      
        try { Try_To_Create_New_Client(); }
        catch (Exception ex)
        {
          Print(ex);
          Print(ex.InnerException);
          client = null;
        }
      else
      {
        Print("Cannot create a new client. The current Client is active.");
      }
    }

    private void Try_To_Create_New_Client()
    {
      try
      {

        NetTcpBinding binding = new NetTcpBinding(SecurityMode.Transport);

        binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
        binding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
        binding.Security.Transport.ProtectionLevel = System.Net.Security.ProtectionLevel.EncryptAndSign;

        string uri = "net.tcp://192.168.1.2:9002/MyService";

        EndpointAddress endpoint = new EndpointAddress(new Uri(uri));

        client = new MyServiceClient(binding, endpoint);

        client.ClientCredentials.Windows.ClientCredential.Domain = "";
        client.ClientCredentials.Windows.ClientCredential.UserName = "Vasya";
        client.ClientCredentials.Windows.ClientCredential.Password = "12345";

        Print("Creating new client ....");
        Print(endpoint.Uri.ToString());
        Print(uri);

        string test = client.Method1("test");

        if (test.Length < 1)
        {
          throw new Exception("Проверка соединения не удалась");
        }
        else
        {
          Print("test is OK  ! " + test);
        }

      }
      catch (Exception ex)
      {
        Print(ex);
        Print(ex.InnerException);
        client = null;
      }
    }

    private void btn_Start_Click(object sender, EventArgs e)
    {
      Create_New_Client();
    }

    private void btn_Send_Click(object sender, EventArgs e)
    {
      Print("sending message . . .");
      string s = textBox1.Text;
      string x = "";
      if (client != null)
      {
        x = client.Method1(s);
        Print(x);
        x = client.Method2(s);
        Print(x);
      }
      else
      {
        Print("Error! Client does not exist!");
      }
    }

    private void btn_Close_Click(object sender, EventArgs e)
    {
      if (client != null)
      {
        Print("Closing a client ...");
        client.Close();
        client = null;
      }
      else
      {
        Print("Error! Client does not exist!");
      }
      this.Close();
    }
  }
}


Компилируем и запускаем с другой машины из той же сети – и тестируем сервис, нажав сначала btn_Start, а затем отправляя сервису сообщения нажатием btn_Send.



Заметим, что в данном примере на клиенте мы совсем не использовали конечную точку

http://localhost:9001/MyService
а работали только с

net.tcp://localhost:9002/MyService
(вы легко сможете это сделать самостоятельно – раз уж вам net.tcp по плечу, то уж http-то с закрытыми глазами сделаете).

Кроме того, мы не использовали файл App.config, создав на клиенте конечную точку не с помощью настроек, а с помощью кода C#. Причины тому – субъективные – автор не любит возиться с XML-настройками, а по возможности всё делает явно в коде. Спасибо за внимание!

Лирическое отступление. Автор статейки познакомился с языком C# в марте сего года, первое приложение на C# написал в мае (до этого много лет формошлёпил на Delphi и даже на MS Access).
Поделиться с друзьями
-->

Комментарии (28)


  1. qw1
    29.06.2017 16:25
    +2

    Интересно, у меня с подобными сервисами были проблемы с правами.
    Если служба запускается под учёткой NT AUTHORITY\Network Service, нужно было специально раздать права

    netsh http add urlacl url=http://+:9001/ user="NT AUTHORITY\Network Service"

    Почему автор с этим не столкнулся?


    1. shai_hulud
      29.06.2017 16:27
      +4

      Когда биндишь localhost, то никакие права не нужны.


  1. eisaev
    29.06.2017 19:07

    После начала эксплуатации аналогичного WCF-сервиса столкнулись с тем, что порт, по которому происходит обмен слушает не приложение/служба, а какой-то системный процесс. Сначала это вызвало некоторый диссонанс при настройке файрвола, а позже выяснилось, что ещё и сниффер (Wireshark) не видит этих пакетов, даже будучи запущенным из под администратора. Сталкивались вы с такими «особенностями»?


    1. user4000
      29.06.2017 19:15

      Нет, с подобными «особенностями», к счастью, не сталкивался, и у меня Wireshark пакеты таки «видит».


    1. mayorovp
      29.06.2017 20:16

      Порт разделяется между всеми процессами, которые его слушают, инструменты вроде netstat показывают случайный процесс. На самом деле, порт слушается драйвером HTTP.SYS


      Пакеты в сниффере должны быть видны, возможно вы как-то не так смотрели.


  1. Szer
    29.06.2017 23:05
    -5

    На дворе 2017ый, а на хабре статьи про WCF (технология без будущего), написанный на VS2015...


    Ожидал увидеть — "Как на netcore-2.0-preview запустить WCF через RabbitMQ"


    1. mayorovp
      30.06.2017 06:09
      +3

      А что не так с этой технологией?


      1. Szer
        30.06.2017 08:41
        -2

        Никакого развития. Поддержки в netcore не предвидится.


        1. mayorovp
          30.06.2017 08:50
          +1

          Вообще-то поддержку в netcore обещали, просто не сразу.


          И да, какое такое развитие вам нужно? Новые спецификации WS каждые полгода не выходят.


      1. ilya-chumakov
        01.07.2017 23:37

        Да с самой технологией всё хорошо. Вот только, как раз благодаря стабильности WCF — ценность новых helloworld-ов на full .NET не так уж и велика. Например, официальная документация по этой теме — на мой взгляд, куда более читабельная. От Хабра всё-таки ждешь глубины, а не полноэкранных скриншотов пустого окна Студии.


    1. FireWall
      30.06.2017 06:54
      +4

      Вы шутите? В корпоративе очень даже используем? WCF как конструктор, что хотите то и реализуете. Как она может быть «без будущего» Что в замен? Заного придумывать велосипед?


      1. Szer
        30.06.2017 08:49
        +1

        Да, мы тоже используем на уровне поддержки. Новые сервисы пишутся как webapi. Для велосипедов сложнее используется akka.net и rabbitmq. Плюсы? Это ещё бОльший конструктор, нет завязки на msmq, можно присоединять другие экосистемы через тот же rabbit.


        У меня вопрос был не к технологии, а к статье, которая по сути хеллоуворлд, да ещё и в 2015 студии. Разницы в коде конечно не будет, но человек явно забыл обновить тулы.


        1. mayorovp
          30.06.2017 08:53
          +1

          В WCF есть биндинг к rabbitmq. Он, конечно, не может использовать все возможности rabbitmq — зато он может использовать все возможности WCF, что также может быть полезным.


      1. CybNat
        30.06.2017 08:58
        -4

        Знаете, и WinForms сейчас все еще много где используется и Silverlight, представьте тоже! Это не показатель! Szer полностью прав, надо идти в ногу со временем! Есть такие технологии как ServiceStack, Protobuf, Rabbit MQ, а вместо того чтобы заниматься броунским движением со службами Windows, можно использовать TopShelf…


  1. aosja
    30.06.2017 09:01
    +2

    А можно и вообще обойтись одним ASP.NET MVC/WEB API. Настраивается Autostart и RunAlways на IIS и не требуется такая гремучая смесь технологий. Плюс возможность создания страниц мониторинга, конфинурации сервиса и проч. Очень удодно и практично.
    MS, конечно, обещал добавить WCF в .NET Core, но изначально ее там не было и они стараются перетянуть всех на ASP.NET MVC (который теперь и Web API)


    1. Naglec
      30.06.2017 14:06
      +3

      И на всех машинах IIS поднимать? Не слишком ли тяжеловесная связка выходит? Кроме того, можно реализовать WCF RESTful службу.


      1. KirillFormado
        30.06.2017 14:23
        +1

        web api можно сделать self hosted


        1. mayorovp
          30.06.2017 14:25

          … и это будет ничем не проще того, что написано тут в статье.


          Суть в том, что без IIS никаких Autostart и RunAlways не настроить, а добавлять IIS в зависимости не всегда допустимо.


          1. KirillFormado
            30.06.2017 14:32

            C TopShelf легко его сделать службой, тут уж как удобно. Если есть возможность использовать iis, отлично. Нет возможности, ну есть вот другой вариант.


            1. mayorovp
              30.06.2017 14:37
              +1

              Вы так пишите, как будто тут описан мега-сложный способ.


      1. aosja
        30.06.2017 14:31

        Нет, не слишком. Слишком, это когда из одного сервиса торчит другой. А еще, из WCF делать RESTful, имея в своем распоряжении ASP.NET стек. Тем более, если из WS-* используется практически ничего. Но, это дело вкуса и других требований.
        На самом деле, нашим LeanOps проще иметь дело с IIS-hosted приложениями, отсюда и переход на ASP.NET c Windows Service. Сначала, мне тоже это казалось странным. Теперь — совсем нет. Кроме того, как я уже говорил, легко прикручиваются очень полезные web-страницы для мониторинга, управления настройками и проч.


        1. Naglec
          30.06.2017 15:48
          +1

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


      1. aosja
        30.06.2017 14:43

        Он (IIS) ваще пихается в Nano Container не задумываясь. Там уменьшение размера идет за счет перехода .NET Core.
        Речь идеть просто об альтернативном подходе. Не мил IIS, не используйте.


  1. QtRoS
    30.06.2017 09:35
    +1

    Кстати, давно хотел спросить и вот подвернулся случай — как правильно обновлять такие службы?
    UPD Обновлять в плане выкатывать версию 2 вместо версии 1, новая сборка с конвейера пришла.


    1. redmanmale
      30.06.2017 18:58
      +2

      Останавливаем службу, заменяем файлы, запускаем службу.


    1. Szer
      30.06.2017 19:04

      Для начала стоит вести версионирование api.
      А деплой подобных компонентов может происходит так:


      1) новая версия деплоится на резервную машину и запускается
      2) балансировщик переключается на резервную машину (все запросы теперь шлются туда)
      3) деплоится новая версия на основную машину
      4) основная становится резервной (опционально)


      Работаем.


      1. QtRoS
        30.06.2017 19:10

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


        1. mayorovp
          30.06.2017 19:59

          Если принципиально "живое" обновление — можно сделать обертку для него.


          Всего-то надо создать второй AppDomain, включить для него теневые копии и поставить FileSystemWatcher, на который повесить перезапуск.