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:
- Utworzyć kontrakty (interfejs)
- Zaimplementować usługę
- Zdefiniować proxy klienckie - klasę dziedziczącą z ClientBase<T> i zwykle implementującą interfejs kontraktu (ale to nie jest wymagane)
- Określić binding (zwykle w pliku konfiguracyjnym)
- Uruchomić proces hostujący – ServiceHost
- 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):
(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ń:
przesyłane komunikaty (z czasem serializacji i deserializacji)
czy po prostu listę aktywności (z czasem)
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)