Przyznam szczerze, że dotychczas z komunikacją między różnymi aplikacjami lub ich warstwami spotykałem się głownie w aspekcie usługowo-webowym. Taki scenariusz sprowadza się do takiego a nie innego wykorzystania webservice lub pochodnego ustrojstwa. Co jednak można zastosować jeżeli chcemy aby dwie nasze aplikacje miały możliwość pogadania ze sobą na lokalnej maszynie?
Rozwiązań jest sporo albo i jeszcze więcej. Poczynając od SendMessage z Win32 API, współdzielonej pamięci, webservice, TCP, COM, DCOM na named pipes kończąc. Jednak chcemy czegoś prostego w implementacji, w końcu programiści są leniwi (w pozytywnym tego słowa znaczeniu
). Postaram się przedstawić dwa scenariusze, z których jeden należy od razu wyeliminować jako obsolete – jednak jako ciekawostkę przytoczę. A drugim będzie WCF – i tą cześć można potraktować jako swoisty quickstart do tej technologii. Mimo że to już nic nowego miałem wenę, aby o tym napisać
Dla ustalenia uwagi przyjmiemy scenariusz, w którym aplikacja A (Host) będzie świadczyć usługi na rzecz aplikacji B (Client), które to sprowadzą się do prostej operacji na łańcuchu znaków i równie prostej operacji pobrania jakiejś paczuszki, która będzie typem nieco bardziej skomplikowanym.
Dawno, dawno temu jak jeszcze .NET miał 1 w numerze wersji istniała technologia .NET Remoting. Ustrojstwo które przypomina w użyciu nieco technologię COM, a obecnie jest niezalecane przez Microsoft na rzecz WCF.
Rzeczą numer jeden jest umowa stanowiąca co oferuje host. Umowę taką definiujemy w postaci kontraktu – interfejsu. Na potrzeby .NET Remoting wystarczy zdefiniować interfejs taki jak na załączonym obrazku:
namespace Contracts
{
public interface ISampleService
{
string ProcessMessage(string message);
Payload GetPayload();
}
[Serializable]
public class Payload
{
public DateTime TimeStamp { get; set; }
public int Value { get; set; }
public Payload()
{
TimeStamp = DateTime.Now;
Value = (new Random()).Next();
}
public override string ToString()
{
return String.Format("{0};{1}", TimeStamp, Value);
}
}
}
Ponieważ Payload jak widać jest paczuszką, która będzie przesyłana między procesami, musi istnieć możliwość jej serializacji – oto tajemnica atrybutu [Serializable].
Niezwykle skomplikowany kod Hosta wygląda następująco:
namespace RemotingHost
{
class Program
{
static void Main(string[] args)
{
var channel = new IpcChannel("RemotingHostChannel");
var serviceType = typeof(SampleService);
ChannelServices.RegisterChannel(channel, false);
RemotingConfiguration.RegisterWellKnownServiceType(serviceType,
"SampleService",
WellKnownObjectMode.Singleton);
Console.WriteLine("Host is running, hit <RETURN> to stop...");
Console.ReadLine();
}
}
public class SampleService : MarshalByRefObject, ISampleService
{
#region ISampleService Members
public string ProcessMessage(string message)
{
int processID = Process.GetCurrentProcess().Id;
Console.WriteLine("{1}: Recieved message: {0}", message, processID);
return String.Format("{1}: your input is \"{0}\"",
message, processID);
}
public Payload GetPayload()
{
int processID = Process.GetCurrentProcess().Id;
var result = new Payload();
Console.WriteLine("{0}: generated payload {1}", processID, result);
return result;
}
#endregion
}
}
Kilka linijek na krzyż i mamy już kawałek aplikacji, która umie świadczyć wybrane usługi klientom. Jako parametr konstruktora klasy IpcChannel podaliśmy nazwę kanału, natomiast do metody RegisterWellKnownServiceType przekazujemy oczywiście typ usługi, jej nazwę oraz określamy czy każde żądanie będzie obsługiwane przez nową instancję klasy implementującej usługę, czy można zadowolić się jedną.
Dziedziczenie SampleService z MarshalByRefObject jest niezwykle istotne, gdyż klient musi mieć możliwośc otrzymania referencji do obiektu usługi, kiedy będzie z niej korzystał.
Reszta nie wymaga komentarza, a w potrzebie zawsze znajdzie się przyjaciel w postaci F1
Klient jest równie prosty w implementacji:
namespace RemotingClient
{
class Program
{
static void Main(string[] args)
{
string message;
var service = (ISampleService) Activator.GetObject(typeof (ISampleService),
String.Format("ipc://{0}/{1}", "RemotingHostChannel",
"SampleService"));
Console.WriteLine("Write something and you'll get a surprise
(empty to exit)");
while (!String.IsNullOrEmpty(message = Console.ReadLine()))
{
Console.WriteLine("{0}", service.ProcessMessage(message));
Console.WriteLine("Your surprise: {0}", service.GetPayload());
}
}
}
}
Chyba jedyną ciekawostką jest tutaj „adres” usługi, w którym rozpoznajemy znajome łańcuchy użyte w kodzie hosta. Wszystko śmiga, różne procesy komunikują się ze sobą i są szcześliwe, jak widać na załączonym obrazku:
Ponieważ, tak jak wspomniałem, .NET Remoting jest obsolete, zrealizujemy tą samą funkcjonalność z wykorzystaniem modnego i lubianego Windows Communication Foundation
WCF jest prosty jak ABC gdzie:
- A = Address, czyli adres pod którą dostępna jest usługa
- B = Binding, czyli sposób w jaki można się z tą usługą porozumieć (protokół, transport jak zwał tak zwał)
- C = Contract, czyli wspomniana wcześniej umowa o świadczenie usług
Kontrakt definiujemy niemal identycznie jak uprzdnio, dodając kilka WCFowych ozdobników:
namespace Contracts
{
[ServiceContract]
public interface ISampleService
{
[OperationContract]
string ProcessMessage(string message);
[OperationContract]
Payload GetPayload();
}
[DataContract]
public class Payload
{
[DataMember]
public DateTime TimeStamp { get; set; }
[DataMember]
public int Value { get; set; }
public Payload()
{
TimeStamp = DateTime.Now;
Value = (new Random()).Next();
}
public override string ToString()
{
return String.Format("{0};{1}", TimeStamp, Value);
}
}
}
Odpowiednie atrybuty instruują WCF co jest czym i co jest dostępne dla klienta. Metody interfejsu które nie są opatrzone [OperationContract] oraz członkowie klasy bez [DataMember] nie będą widoczne po drugiej stronie barykady.
Istotna rzecz: jakby ktoś z jakiegoś niewyjaśnionego powodu chciał łączyć WCF z .NET Remoting – dobra nowina, .NET Remoting może korzystać z tej samej definicji kontraktu co WCF. Ozdobniki nie stanowią przeszkody.
Ponieważ chcemy komunikować się między procesami na tej samej maszynie, użyjemy efektywnego transportu named pipes.
I tak oto prezentuje się kod hosta:
namespace WCFHost
{
class Program
{
static void Main(string[] args)
{
using (var host = new ServiceHost(typeof(SampleService),
new Uri("net.pipe://localhost")))
{
host.AddServiceEndpoint(typeof(ISampleService), new NetNamedPipeBinding(), "SampleService");
host.Open();
Console.WriteLine("Host is running, hit <RETURN> to stop...");
Console.ReadLine();
host.Close();
}
}
}
public class SampleService : ISampleService
{
#region ISampleService Members
public string ProcessMessage(string message)
{
int processID = Process.GetCurrentProcess().Id;
Console.WriteLine("{1}: Recieved message: {0}", message, processID);
return String.Format("{1}: your input is \"{0}\"",
message, processID);
}
public Payload GetPayload()
{
int processID = Process.GetCurrentProcess().Id;
var result = new Payload();
Console.WriteLine("{0}: generated payload {1}", processID, result);
return result;
}
#endregion
}
}
Kod sam się objaśnia, zgodnie z WCFowym ABC konigurowana jest usługa. Warto zwrócić uwagę, na to iż klasa SampleService nie dziedziczy juz po MarshalByRefObj.
Klient równie prosty:
namespace WCFClient
{
class Program
{
static void Main(string[] args)
{
var factory = new ChannelFactory<ISampleService>(new NetNamedPipeBinding(),
new EndpointAddress("net.pipe://localhost/SampleService")
);
var factory = new ChannelFactory<ISampleService>("SampleService");
var service = factory.CreateChannel();
string message;
Console.WriteLine("Write something and you'll get a surprise
(empty to exit)");
while (!String.IsNullOrEmpty(message = Console.ReadLine()))
{
Console.WriteLine("{0}", service.ProcessMessage(message));
Console.WriteLine("Your surprise: {0}", service.GetPayload());
}
}
}
}
I jak widać na kolejnym obrazku: znów działa
Proste jak ABC, czyż nie?
STOP!
Może być jeszcze prostsze a przy okazji bardziej elastyczne. WCF został tak zaprojektowany, aby każdą usługę i klienta można było dynamicznie konfigurować bez potrzeby rekompilacji kodu.
Zmieńmy zatem aplikację hosta dodając do niej plik konfiguracyjny:
<configuration>
<system.serviceModel>
<services>
<service name="WCFHost.SampleService">
<endpoint address="net.pipe://localhost/SampleService"
binding="netNamedPipeBinding"
contract ="Contracts.ISampleService"/>
</service>
</services>
</system.serviceModel>
</configuration>
Teraz host jest jeszcze prostszy i czytelniejszy:
namespace WCFHost
{
class Program
{
static void Main(string[] args)
{
using (var host = new ServiceHost(typeof(SampleService)))
{
host.Open();
Console.WriteLine("Host is running, hit <RETURN> to stop...");
Console.ReadLine();
host.Close();
}
}
}
}
Klasa ServiceHost automatycznie na podstawie nazwy typu usługi (typeof) wspomnianej w atrubycie name elementu
Z klientem sytuacja jest analogiczna, konfiguracja:
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.pipe://localhost/SampleService"
contract="Contracts.ISampleService"
binding="netNamedPipeBinding"
name="SampleService"/>
</client>
</system.serviceModel>
</configuration>
I oczyszczony kod:
namespace WCFClient
{
class Program
{
static void Main(string[] args)
{
var factory = new ChannelFactory<ISampleService>("SampleService");
var service = factory.CreateChannel();
string message;
Console.WriteLine("Write something and you'll get a surprise
(empty to exit)");
while (!String.IsNullOrEmpty(message = Console.ReadLine()))
{
Console.WriteLine("{0}", service.ProcessMessage(message));
Console.WriteLine("Your surprise: {0}", service.GetPayload());
}
}
}
}
Prawda że ładniej?
Oczywiście to co opisałem jest wirzchołkiem góry lodowej jeżeli chodzi o WCF, ale mam głęboką nadzieję że może jakiejś zbłąkanej duszy pozwoli rozpocząć przygodę z WCF w bezbolesny sposób, bez konieczności przekopania się przez miliony stron opisów na początek.
Ponieważ zakochałem się z wzajemnością w Git, kod źródłowy przykładów możecie znależć na git://github.com/emdzej/blog_samples_ipc.git

