Im letzten Post wurden die Grundlagen der Windows Services geklärt. Nun ist es notwendig unser Beispiel zu erweitern, denn was bringt uns ein Service der Daten aufzeichnet, sie aber nicht weitergeben kann? Das Ziel ist also klar: Wir wollen die gemessene Zeit!

IPC = Inter Process Communication

IPC beschreibt grundsätzlich die Kommunikation verschiedener Prozesse auf ein und derselben Maschine. Diese Kommunikation ist etwas aufwendiger, da jeder Prozess mindestens eine eigene Application Domain besitzt und jede Application Domain eine eigene Ressourcenverwaltung hat. Dies betrifft geöffnete Dateien, Sicherheitseinstellungen und vieles mehr. Verwendet man also unterschiedliche Domänen muss man eine Brücke zwischen ihnen schlagen da man nicht ohne Weiteres im gleichen Speicher herum vorwerken kann.

Diese Brücke kann dabei verschiedenste Ausprägung haben. Sie kann zum Beispiel ein gemeinsam genutztes Objekt sein (erinnert an Shared Memory) oder ein gemeinsam genutzter Kommunikationskanal (erinnert an Pipes). Das Beispiel ist im Grunde eine Mischung aus beidem.

Planung

Vor der eigentlichen Entwicklung kommt die Planung. Hier zunächst unsere bisherigen Klassen: Quellcode eines eigenen Windowsdienstes (1524)

Programm dient dem Programmstart, MyInstaller der Installation des Services und MyTimeService enthält die tatsächliche Funktionalität, das Ermitteln der Zeit.

Was benötigen wir also zusätzlich? Zunächst bedarf es einer Klasse die die Kommunikation initialisiert und verwaltet d.h. eine Art Serverobjekt (IpcDataProvider). Dem gegenüber braucht es einen Client der die Daten entgegen nimmt (IpcDataConnector) und einen Container der die zu übertragenden Werte kapselt (TimeValueContainer).

IpcDataPresenter und IpcDataProvider packen wir in ein extra Projekt vom Typ Class Library, welches wir „IpcConnectionHelper“ nennen. Durch ihren Aufbau und die Auslagerung können wir sie auch in zukünftigen Projekten verwenden. TimeValueContainer kommt ebenfalls in ein eigenes Projekt „Contracts“ vom gleichen Typ um es in der Clientapplikation und dem Service nutzen zu können.

Implementierung der Container

Jetzt geht es endlich ans Bauen. Der Aufbau des Interfaces und Containers ist sehr einfach. Der Grund warum das Interface keinen Setter verlangt ist darin begründet, dass Abonnenten unseres Dienstes den Wert nicht setzen können sollen. Wichtig für den Container ist außerdem die Elternklasse MarshalByRefObject nur durch sie ist es später möglich eine Referenz des Objekts über Domänengrenzen hinweg zu übergeben.

public class TimeValueContainer : MarshalByRefObject
{
    public DateTime Value {get; set; }
}

Der Server

Nun zum tatsächlich Neuen, dem Server. Um diesen erstellen zu können muss man zunächst eine Referenz auf System.Runtime.Remoting hinzufügen.

Die gesamte Kommunikation läuft bei IPC über sogenannte Named Pipes. Diese kann man sich als Kanäle mit einem bestimmten Namen vorstellen. Wer den Namen kennt, kann Daten in diese Kanäle schieben bzw. aus ihnen heraus holen. Die Abstraktionsebene des Frameworks ist an dieser Stelle aber so hoch, dass wir eigentlich nicht all zu viel davon mit bekommen, denn alles was wir später austauschen sind Referenzen auf Proxyobjekte. Die letztendliche Übertragung läuft im Hintergrund ab.

Damit wir unsere Daten übertragen können, müssen wir bestimmen wie dies geschehen soll. In unserem Fall habe ich das Binärformat gewählt, weil es mir am schnellsten erscheint. Eine Alternative wäre noch der SOAP-Formatter der explizit für verteilte Anwendungen entworfen wurde die nicht zwangsläufig auf Basis von .NET arbeiten.

var serverSinkProvider = new BinaryServerFormatterSinkProvider();
serverSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
var clientSinkProvider = new BinaryClientFormatterSinkProvider();

Um die Kommunikation auszuführen wird ein Kanal vereinbart. Die Unterscheidung bei den Kanalklassen, wie sie im Framework zu finden sind, liegt daran, dass der Kanal für den Server andere Funktionalitäten bereit hält als für den Client. So wird im oberen Beispiel dem ServerChannel über TypeFilterLevel mitgeteilt, dass er alle Typen, auch die von uns selbst erstellten, akzeptieren soll. Ohne diese Einstellung könnten wir unseren Container nicht verteilen sondern müssten die Zeitwerte direkt übertragen.

Als nächstes erstellen wir den tatsächlichen Kanal. Dazu müssen wir zunächst auf etwas umständliche Art und schlecht beschriebene Weise alle notwendigen Properties setzen. Laufen die Prozesse unter dem gleichen Benutzerkonto, reicht es den Namen des Kanals anzugeben. Ansonsten muss man sich der hier beschriebenen Properties bedienen.

Da unser Dienst unter dem System Account läuft geben wir standardmäßig Programmen die in einem Konto der Administratorengruppe laufen das Recht auf den Kanal zuzugreifen (Property „authorizedGroup“). Danach wird der neu erschaffene Kanal mit ChannelServices.RegisterChannel registriert. Der zweite Parameter dieser Funktion hat für IPC keine Bedeutung, muss aber gesetzt werden…

var properties = new Dictionary<string, string>();
properties.Add("portName", channelName);
properties.Add("authorizedGroup", "Administratoren");
_channel = new IpcChannel(properties, clientSinkProvider, serverSinkProvider);
ChannelServices.RegisterChannel(_channel, true);

Nun bauen wir unseren Shared Memory auf. Über die RemotingConfiguration Klasse registrieren wir den Typ unseres Containers (targetType) beim Betriebssystem unter einem bestimmten Namen (targetRemoteName). Dieser sollte nach Möglichkeit einzigartig sein, behandelt er doch all unsere Anfragen. Genau aus diesem Grund wird er auch als Singleton angelegt, denn ohne diese Einstellung würde jedes Mal eine neue Instanz erstellt werden wenn wir sie anfordern.

RemotingConfiguration.RegisterWellKnownServiceType
   (targetType, targetRemoteName, WellKnownObjectMode.Singleton);

Die eigentliche Instanz des Containers bzw. seines Proxyobjekts erzeugen wir mit Hilfe des Activators und Reflection. Dafür ist es zunächst notwendig aus dem Namen des Kanals und dem globalen Namen unseres Typs eine URL zu bauen. Diese wird dem Activator zusammen mit dem „lokalen“ Typ übergeben. Mit diesen Informationen kann er dann einen Proxy bauen der alle Anfragen automatisch verarbeitet. Deshalb ist es nicht möglich eine Instanzierung mit new vorzunehmen. Dies würde nur ein Objekt des lokalen Typs erstellen.

string url = string.Format("ipc://{0}/{1}",channelName, targetRemoteName);
RemoteObject = Activator.GetObject(targetType, url);

Ein Tipp: Hat man vergessen die Klasse von MarshalByRefObject abzuleiten kommt bei installiertem deutschen Framework eine Exception mit der  Nachricht „Es wurde versucht, einen Proxy für einen ungebundenen Typ zu erstellen.“ sobald man versucht die Klasse zu instanzieren

Gesamter Code des Servers:

public class IpcDataProvider
{
   public object RemoteObject { get; private set; }
   private IpcChannel _channel;
   public object Initialize(string channelName, Type targetType,
                            string targetRemoteName)
   {
       if (_channel == null)
       {
           // create binary formatter to serialize data
           var serverSinkProvider = new BinaryServerFormatterSinkProvider();
           serverSinkProvider.TypeFilterLevel = TypeFilterLevel.Full;
           var clientSinkProvider = new BinaryClientFormatterSinkProvider();
           // configure named pipe
           var properties = new Dictionary<string, string>();
           properties.Add("portName", channelName);
           properties.Add("authorizedGroup", "Administratoren");
           _channel = new IpcChannel(properties, clientSinkProvider,
                                     serverSinkProvider);
           ChannelServices.RegisterChannel(_channel, true);
           // register object as singleton
           RemotingConfiguration.RegisterWellKnownServiceType(targetType,
                     targetRemoteName, WellKnownObjectMode.Singleton);
           // get instance on singleton object
           string url = string.Format("ipc://{0}/{1}", channelName,
                                      targetRemoteName);
           RemoteObject = Activator.GetObject(targetType, url);
      }
      return RemoteObject;
   }
}

Der Client

Nachdem wir wissen wie der Server aufgebaut wird ist der Client sehr einfach und bedarf kaum Erläuterung. Wichtig ist an dieser Stelle nur, dass wir neben dem Kanalnamen des Servers, den wir brauchen um auf den Shared Memory zuzugreifen, auch einen einzigartigen für den Client brauchen. Nutzen wir einen Namen für den Clientkanal der schon vorhanden ist, fliegen die Exceptions.

Gesamter Code des Clients:

public class IpcDataConnector
{
   public object RemoteObject { get; private set; }
   private IpcChannel _channel;
   public object Establish(string clientChannelName,
                           string serverChannelName,
                           Type targetType, string targetRemoteName)
   {
       if (_channel == null)
       {
           _channel = new IpcChannel(clientChannelName);
           ChannelServices.RegisterChannel(_channel, false);
           string url = string.Format("ipc://{0}/{1}", serverChannelName,
                                                       targetRemoteName);
           RemoteObject = Activator.GetObject(targetType, url);
        }
     return RemoteObject;
    }
}

Umbau des Service

Der Umbau des Services hält sich in Grenzen. Zunächst fügen wir ihm zwei Felder hinzu, eines für den IpcDataProvider und eines für den Proxy unseres Shared Memories. Daraufhin wird der Konstruktor so angepasst, dass beide initialisiert werden. Es ist dabei wichtig die Referenz auf den DataProvider zu halten, ansonsten wird er vom Garbagecollector beräumt und der Übertragungskanal wieder abgebaut!!!

_ipcServer = new IpcDataProvider();
_ipcServer.Initialize("MyTimeServiceChannel", typeof (TimeValueContainer),
                                                     "TimeValueContainer.rm");
_valueContainer = _ipcServer.RemoteObject as TimeValueContainer;

Den Container verwenden wir dann in unserer TimeValue Property, alles andere macht das Framework.

private DateTime TimeValue
{
   get { return _valueContainer.Value; }
   set { _valueContainer.Value = value; }
}

Wie man sieht, kann man ohne weiteres über den Proxy auf die Daten zugreifen, ganz so als sei es ein lokales Objekt. Die gesamte dahinter laufende Kommunikation ist versteckt und der Wartungsaufwand hält sich in Grenzen.

Die Implementierung des Clients unterscheidet sich kaum. Statt eines Providers wird der Connector angelegt, dessen Referenz danach unbedingt gehalten werden muss. Ein passender Aufruf der Establish Methode von IpcDataConnector könnte so aussehen:

_dataConnector.Establish("MyTimeServiceChannel", typeof (TimeValueContainer),
                                                        "TimeValueContainer.rm");

Refactoring /Generalisierung

Bei der aktuellen Implementierung haben wir zwei Probleme: Wir werden nicht automatisch über Werteänderungen informiert und die Lösung mit der Contracts.dll ist unsauber. So kann zum Beispiel der Client Daten in den Container schreiben und damit Fehler verursachen.

Zunächst wird also statt des TimeValueContainers ein Interface eingeführt welches vorschreibt, dass es einen Wertecontainer geben muss der Daten eines bestimmten Typs hält. Das setzen dieser Daten ist implizit verboten da wir an dieser Stelle nur eine 1 : n und keine m : n Beziehung zulassen wollen. Oder anders: Der Server setzt die Werte, sonst keiner!

public interface IRemoteDataContainer<T>
{
   T ContainedData { get; }
}

Weiterhin wollen wir die Möglichkeit geben, Events auszulösen wenn Daten geändert werden. Da dies nicht zwangsläufig für alle RemoteContainer gelten soll, wird ein weiteres Interface deklariert.

public interface IObserveableRemoteObject<T> : IRemoteDataContainer<T>
{
   event EventHandler DataChangedEvent;
}

Auf Serverseite kann nun erneut eine TimeValueContainer-Klasse eingeführt werden die eben jene Schnittstellen realisiert. Der Client muss diese nicht kennen, da er sich mit IRemoteDataContainer begnügt. Letztendlich geht es ihm ja nicht um den Container, sondern die gekapselten Daten. Aus dem gleichen Grund unterscheiden sich nun auch Connector und Provider in größerem Maße als zuvor. Denn während der DataProvider eine Property RemoteObject vom generischen Typ T  besitzt, hat der DataConnector die gleiche vom Typ IRemoteDataContainer.

Dies hat auch zur Folge, dass sich Initialisierung maßgeblich unterscheidet:

IpcDataProvider<TimeValueContainer> _provider;
IpcDataConnector<DateTime> _connector;

Intern muss bei Initialize und Establish nur der Typ gegen ein entsprechendes typeof() ausgetauscht werden. Wie das geht spare ich mir jetzt und kann dem angehangenen Quellcode entnommen werden.

Eventbehandlung

Viel interessanter ist nämlich die Behandlung von Events. Wobei die hier vorgestellte Lösung leider nur funktioniert wenn Service und Client unter dem gleichen lokalen Benutzerkonto laufen. Möchte man unterschiedliche Nutzerkonten verwenden kommt man wohl um die Verwendung von TCP nicht herum.

Folgt man der bisherigen Anleitung ist man leicht versucht, einfach das Event des Containers zu abbonieren. Dies schlägt jedoch fehl! Der Grund dafür ist das Marshaling, welches das Mappen von Objekten und Typen aus einer Umgebung heraus in eine andere bezeichnet. Als Umgebungen können hier zum Beispiel, wie in unserem Fall, zwei managed AppDomains gemeint sein. Es kann aber auch das Mappen von CLR Datentypen auf native bedeuten wenn es um das Thema COM Interoperabilität geht.

Bisher ist uns Mashaling durch die Basisklasse MarshalByRefObject über den Weg gelaufen und genau sie brauchen wir jetzt wieder. Denn wir müssen eine Brücke zwischen den Events in der einen AppDomain und ihren Handlern in der anderen schaffen. Sprich, es muss eine Übersetzung statt finden.

Dem RemoteEventListener wird ein IObervableRemoteObject übergeben, dessen Event er abboniert. Feuert das Objekt jenes Ereignis, nimmt er es entgegen und leitet es bildlich gesprochen in die AppDomain des Clients.

[Serializable()]
public class RemoteEventListener<T> : MarshalByRefObject
{
   private EventHandler _eventHandler;
   private IObserveableRemoteObject<T> _observableObject;
   public RemoteEventListener()
   {
      _eventHandler = new EventHandler(InvokeDatachangedEvent);
   }
   public IObserveableRemoteObject<T> ObservableObject
   {
      get { return _observableObject; }
      set
      {
         if(value == null)
            throw new ArgumentException("value");
         if (value != _observableObject &&
             _observableObject != null)
         _observableObject.DataChangedEvent -= _eventHandler;
         _observableObject = value;
         _observableObject.DataChangedEvent += _eventHandler;
     }
   }
    public event EventHandler DataChangedEvent;
    private void InvokeDatachangedEvent(object sender, EventArgs eventArgs)
    {
        EventHandler handler = DataChangedEvent;
        if (handler != null)
            handler(sender, eventArgs);
    }
}

Auch wenn der EventListener von MarshalByRefObject abgeleitet ist, so sollte er nicht wie vom TimeValueContainer bekannt, global registriert werden. Denn dann geschehe die Ereignisbehandlung sozusagen außerhalb der AppDomain des Clients und dies führt zu einem Serialisierungsfehler mit entsprechender Exception. Stattdessen wird der Listener vom IpcDataConnector für jeden Client per new erzeugt und mit dem IObservableRemoteObject versehen.

EventListener = new RemoteEventListener<T>();
EventListener.ObservableObject = RemoteObject as IObserveableRemoteObject<T>;

Mögliche Weiterentwicklung

Mit der bisherigen Lösung ist nicht möglich Daten über ein Netzwerk zu übertragen da Named Pipes nur auf ein und demselben Rechner existieren können. Dies kann man umgehen indem man keinen IPC Channel verwendet, sondern einen TCP Channel. Die Implementierung ist fast die gleiche.

Alles hat ein Ende

So interessant die hier besprochenen Dinge sind um so ernüchternder ist es, dass uns das Wissen in Zukunft nicht mehr ganz so viel bringen wird. Denn laut MSDN ist in Zukunft auf die Windows Communication Foundation zurück zu greifen, wann immer man Prozesse mit einander kommunizieren lassen möchte. Dennoch finde ich die hier gezeigt Lösung eine einfache und schnelleinsetzbare (mit den von mir bereitgestellten Klassen) Alternative bevor man sich auf die WCF einlässt.

Quellcode: Windows Service mit IPC (446)
Kick It auf dotnet-kicks.de