Pisząc rozwiązanie, nie zawsze wydzielane są formalne warstwy z fizyczną separacją komponentów. Czasami taka separacja ma miejsce na poziomie klas (czy modułów) w środku aplikacji. W takim scenariuszu warto mieć narzędzie, które zapewni dodatkową infrastrukturę – typu transakcje, sesja, bezpieczeństwo itp. Dodatkowo, warto móc sprawdzić jak taka komunikacja wygląda, ile kosztuje odwołanie do takiego modułu – czy po prostu pozwoli na diagnostykę “środka” aplikacji.

Aby to uzyskać, można użyć Windows Communication Foundation (WCF) – ale w taki sposób że “serwer” i “klient” będą się znajdowały w jednym AppDomain (albo nawet w jednym Assembly)

Do komunikacji najwygodniej jest wykorzystać binding <netNamedPipeBinding>, który jest zoptymalizowany pod kątem komunikacji międzyprocesowej, ale może tez być użyty jako narzędzie do komunikacji “InProc” – w ramach jednego AppDomain. Standardowo, wykorzystuje on mechanizm WS-ReliableMessaging (by zapewnić pewność dostarczenia komunikatu i synchronizację), bezpieczeństwo na poziomie transportu, przesyłane informacje serializuje w sposób binarny a do komunikacji wykorzystuje nazwane potoki (precyzyjniej – IPC).

Aby taki mechanizm wykorzystać, należy:

  1. Utworzyć kontrakty (interfejs)
  2. Zaimplementować usługę
  3. Zdefiniować proxy klienckie - klasę dziedziczącą z ClientBase<T> i zwykle implementującą interfejs kontraktu (ale to nie jest wymagane)
  4. Określić binding (zwykle w pliku konfiguracyjnym)
  5. Uruchomić proces hostujący – ServiceHost
  6. Wywołać daną funkcjonalność za pośrednictwem proxy

W naszym przypadku zdefiniujemy kontrakt, który będzie miał 2 metody. Jedna (CalculateF) wylicza liczbę Fibonacciego. Druga, (Add) dodaje dwie liczby i będzie używana do pokazania narzutu jaki daje taki sposób komunikacji (narzut – uprzedzając dalsze rozważania, jest w praktyce pomijalny).

Kontrakt ma postać następującą:

    [ServiceContract]
    interface ISimpleService {
        [OperationContract]
        long CalculateF(long n);
        [OperationContract]
        int Add(int a,int b);
    }

Implementacja kontraktu też nie jest niczym nadzwyczajnym:

    class MySimpleService:ISimpleService {
        #region ISimpleService Members
        public long CalculateF(long n) {
            if (n <= 1) return n;
            return (CalculateF(n - 1) + CalculateF(n - 2));
        }
        public int Add(int a, int b) {
            return a + b;
        }
        #endregion
    }

Definicja proxy ma następującą postać:

    partial class MySimpleServiceClient : ClientBase<ISimpleService>, ISimpleService {
        public MySimpleServiceClient () { }
        public MySimpleServiceClient(string configurationName): base(configurationName) { }
        #region ISimpleService Members
        public long CalculateF(long n) {
            return Channel.CalculateF(n);
        }
        public int Add(int a, int b) {
            return Channel.Add(a, b);
        }
        #endregion
    }

W tym przypadku nie wykorzystujemy mechanizmu automatycznego generowania proxy, tylko piszemy je ręcznie. Proszę zauważyć, że dziedziczenie z ClientBase<T> daje nam od razu obiekt Channel który pozwala komunikować się z usługą. Zwykle, dla wygody impelementuje się jeszcze interfejs kontraktu – by klient po prostu wywoływał konkretną metodę.

Kolejnym ważnym elementem jest zdefiniowanie bindingów – zarówno dla klienta jak i dla serwisu(w jednym app.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="WCF35SP1_InsideExe.MySimpleService">
        <endpoint address="net.pipe://localhost/MySimpleService" binding="netNamedPipeBinding" contract="WCF35SP1_InsideExe.ISimpleService" />
      </service>
    </services>
    <client>
      <endpoint address="net.pipe://localhost/MySimpleService" binding="netNamedPipeBinding" contract="WCF35SP1_InsideExe.ISimpleService" />
    </client>
  </system.serviceModel>
</configuration>

Proszę zauważyć, że definiujemy zarówno <service> jak i <client> – wskazując odpowiednie klasy i interfejsy w naszym EXE (WCF35SP1_InsideExe to nazwa projektu i przestrzeni w tym przykładzie)

Należy jeszcze pamiętać, że “host” musi być fizycznie uruchomiony. Tu zwykle wykorzystywany jest ServiceHost. Nie ma potrzeby definiować opcji bindingu (bo są one zdefiniowane w app.config), należy tylko wskazać jaki typ będzie hostowany. Dzięki temu ServiceHost wyszukuje definicję stosownego endpointu i uruchamia “nasłuch”.

ServiceHost service1 = new ServiceHost(typeof(MySimpleService));
service1.Open();

Po tych krokach można  wykonać kod, na przykład taki:

using (MySimpleServiceClient clt = new MySimpleServiceClient()) {
    Console.WriteLine(clt.Add(10, 20)); 
    Console.WriteLine(clt.CalculateF(5)); 
    Console.WriteLine(clt.CalculateF(10));
    Console.WriteLine(clt.CalculateF(15)); 
}

W ten sposób zdefiniowana została i wywołana warstwa “serwisu” w środku jednego pliku EXE.

Dzięki takiej konfiguracji, możemy na przykład dokładnie zobaczyć koszty i komunikację pomiędzy “warstwami”. Można w pliku konfiguracyjnym ustawić:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
        <listeners>
          <add type="System.Diagnostics.DefaultTraceListener" name="Default">
            <filter type="" />
          </add>
          <add name="ServiceModelMessageLoggingListener">
            <filter type="" />
          </add>
        </listeners>
      </source>
      <source name="System.ServiceModel" switchValue="Warning, ActivityTracing"
        propagateActivity="true">
        <listeners>
          <add type="System.Diagnostics.DefaultTraceListener" name="Default">
            <filter type="" />
          </add>
          <add name="ServiceModelTraceListener">
            <filter type="" />
          </add>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add initializeData="C:\TS\PREZENTACJE\WCF35SP1_InsideExe\WCF35SP1_InsideExe\App_messages.svclog"
        type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        name="ServiceModelMessageLoggingListener" traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, ProcessId, ThreadId, Callstack">
        <filter type="" />
      </add>
      <add initializeData="C:\TS\PREZENTACJE\WCF35SP1_InsideExe\WCF35SP1_InsideExe\App_tracelog.svclog"
        type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
        name="ServiceModelTraceListener" traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, ProcessId, ThreadId, Callstack">
        <filter type="" />
      </add>
    </sharedListeners>
  </system.diagnostics>
  <system.serviceModel>
    <diagnostics>
      <messageLogging logMalformedMessages="true" logMessagesAtTransportLevel="true" />
    </diagnostics>
    <services>
      <service name="WCF35SP1_InsideExe.MySimpleService">
        <endpoint address="net.pipe://localhost/MySimpleService" binding="netNamedPipeBinding" contract="WCF35SP1_InsideExe.ISimpleService" />
      </service>
    </services>
    <client>
      <endpoint address="net.pipe://localhost/MySimpleService" binding="netNamedPipeBinding" contract="WCF35SP1_InsideExe.ISimpleService" />
    </client>
  </system.serviceModel>
</configuration>

Analogiczne operacje można wykonać po wybraniu Tools – WCF Service Configuration Editor (wtedy jest prościej):

image

(w tym przypadku włączone są wszystkie elementy poza komunikatami serwisowymi (które przydają się tylko przy bardzo specyficznej diagnostyce). Logowane informacje są zapisywane w 2 plikach App_messages.svclog (fizyczne komunikaty przesyłane przez WCF) i App_tracelog.svclog (śledzenie aktywności).

Poniżej można zobaczyć graf wywołań:

image 
przesyłane komunikaty (z czasem serializacji i deserializacji)

image

czy po prostu listę aktywności (z czasem)

image

Narzędzie do analizy plików *.svclog znajduje się w <programfiles>\Microsoft SDKs\Windows\v6.0A\bin\SvcTraceViewer.exe. Tam także znajduje się plik z dokumentacją (SvcTraceViewer.chm) którą warto przejrzeć.

Wydajność

Warto pamiętać, że taka technika nie nadaje się do zastępowania każdego wywołania metody w kodzie. Może być zastosowana pomiędzy logicznymi granicami w aplikacji. Użycie WCF “kosztuje” na jednym wywołaniu metody około 3 ms. Dokładniej, na procesorze Intel T7500 samo wywołanie metody w klasie C# trwa 0,00002 ms. Wywołanie przez WCF zajmuje “aż” 2,8691 ms.

Ale – proszę zobaczyć, że jeżeli ta metoda liczy coś bardziej skomplikowanego (na przykład – tu ciąg Fibbonaciego), to się okazuje że ten “narzut” na wywołanie nie jest aż tak bardzo istotny. Bezpośrednie wywołanie to na przykład 69,763 ms, a przy użyciu WCF czas wzrasta do (średnio) 74,357 ms.

Natomiast warto pamiętać, że aby dodać kontekst bezpieczeństwa, sesje, pilnować by nie powstało “za dużo” obiektów danego typu – to konieczne jest samodzielne zakodowanie stosownych algorytmów – które w WCF są gotowe, a dodatkowo ta implementacja jest bardzo wydajna (i skalowalna). Oczywiście, nie zachęcam by w całej aplikacji wymienić wszystkie wywołania na takie z wykorzystaniem WCF. Ale – tam gdzie mamy jakieś granice (typu moduł, czy jakiś blok funkcjonalny w kodzie), warto wykorzystać taką technologię, bo będzie łatwiej chociażby monitorować aplikacje

Pełne wyniki prostego testu:

Proste dodawanie, dla 100000 iteracji 
Wywołanie bezpośrednie: 2 ms 
Wywołanie bezpośrednie/sztukę: 0,00002 ms 
Wywołanie proxy: 286910 ms 
Wywołanie jednej metody: 2,8691 ms 
Ciąg Fibbonaciego, dla 1000 iteracji 
Wywołanie bezpośrednie: 69763 ms 
Wywołanie bezpośrednie/sztukę: 69,763 ms 
Wywołanie proxy: 74357 ms 
Wywołanie jednej metody: 74,357 ms 

Pełny przykład:

WCF_InsideExe.zip (83,06 kb)