.NET 2.0: "Nur eine Instanz pro Anwendung" featuring IPC

von Christian Stelzmann


Dieser Artikel basiert auf dieser Veröffentlichung: IPC Remoting "Real World" Example

Für diesen Artikel habe ich ein Demo-Projekt erstellt, auf welches ich auch Bezug nehmen werde. Sie sollten es sich hier herunterladen.



Einleitung

Es kommen immer wieder Anwendungen vor, bei denen man erreichen möchte, dass nur eine Anwendungs-Instanz von ihnen existiert. Sowohl unter Win32 als auch unter .NET kann man dies mit einem Mutex erreichen, in beiden Welten geht es gleich einfach. Vor einem Problem steht man unter .NET, wenn man Daten von der neuen Anwendungs-Instanz an die bestehende Instanz übergeben möchte, z.B. Aufrufparameter.

Unter Win32 konnte dies z.B. relativ einfach mittels WM_COPYDATA erreicht werden. Diese Möglichkeit bleibt einem unter .NET verschlossen. Unter .NET 1.1 war es nötig, mittels TCP einen Server und einen Client aufzumachen, welche sich dann Daten schickten. .NET 2.0 bietet für die Kommunikation auf einem einzelnen Rechner entsprechende IPC-Klassen, welche deutlich schneller als TCP arbeiten.

Wie man dies nutzt, um Aufrufparameter von einer Anwendungs-Instanz zur anderen zu schaufeln, sei im folgenden gezeigt. Ich werde hierbei auf die im Anhang zu findende Anwendung eingehen.


Trockenübung

Zur Kommunikation zwischen den beiden Anwendungs-Instanzen wird ein spezielle Klasse verwendet. An diese Klasse werden kaum Anforderungen gestellt, einzig von MarshalByRefObject muss sie abgeleitet sein Kurz gesagt kann man dadurch über Applikations-Grenzen hinweg auf ein und dasselbe Objekt zugreifen! Ein wichtiger Baustein also bei der Kommunikation zwischen zwei Anwendungs-Instanzen.

Man kann sich dieses Objekt nun als Vermittler zwischen den beiden Anwendungs-Instanzen vorstellen. Intern arbeiten dabei sogenannte "Marshaler", die das Objekte serialisieren bzw. deserialisiern. Beim Serialisieren werden einfach alle Eigenschaften des Objektes in einen Speicherblock "gestopft", welcher somit den Zustand des Objektes zum aktuellen Zeitpunkt reprsäntiert. Dieser Speicherblock wird nun einfach in den anderen Prozess kopiert und deserialisiert, womit man ein Objekt erhält, dessen Zustand dem aus dem anderen Prozess entspricht.

Erstellt wird dieses Objekt von der ersten Instanz. Dabei soll dem Objekt ein Delegat übergeben werden (eine Methode in der ersten Instanz), welcher (via Objekt) von der zweiten Instanz aufgerufen werden kann.

Die zweite Anwendungs-Instanz stelllt also erst einmal mittels Mutex fest, dass sie nicht die erste ihrer Art ist. Dann holt sie sich das (durch die erste Anwendungs-Instanz erstellte und mit Delegat bestückte) Objekt und ruft den Delegaten auf. Der Delegat erhält dabei als Parameter die Aufrufparameter, welche an die erste Anwendungs-Instanz übergeben werden sollen.

Der Aufruf des Delegaten ist die Brücke in die erste Anwendungs-Instanz. Sie kann nun den Parameter verarbeiten.


Wie es geht ...

Der erste Start

Ist die Anwendung also vorschriftsmäßig gestartet, muss sie nun (in "Form_Load" in der Datei "Form1.cs") das "Vermittlunsgobjekt" (kein Fachbegriff ;-)) erstellen. Dazu würde man normalerweise einen so genannten IpcChannel erstellen (zur Datenübertragung) und dann den Typen des Vermittlungsobjektes als Dienst dieses Channels registrieren.

Leider soll das Vermittlungsobjekt ja einen Delegaten erhalten, was die Sache etwas komplizierter macht. Bei einer "normalen" Erstellung eines Channels, bei der einfach nur ein Name festgelegt wird, kann ein solcher Delegat nicht korrekt serialisiert werden, was eine Voraussetzung dafür ist, dass die ganze Sache funktioniert.

An dieser Stelle war der ganz oben verlinkte Artikel eine gigantische Hilfe, im Prinzip der Schlüssel zum Ganzen. In ihm wird demonstriert, dass man dem Channel ein Objekt zur Verfügung stellen kann, welches für die korrekte Serialisierung sorgt. Es hat den handlichen Namen "BinaryServerFormatterSinkProvider".

Über MesssageSinks kann man sich hier informieren. Um eine kurze Vorstellung hier eine recht unfachliche Beschreibung: Eine Nachricht, welche sich durch einen Channel vom Client zum Server bewegt, passiert dabei MessageSinks. Diese verarbeiten diese Nachricht und verändern diese dabei. Jeder Sink tut dabei andere Dinge. Der hier vorliege Sink sorgt halt für eine korrekte Serialisierung.

Mittels dieses Objektes kann man nun einen Channel erstellen, der auch Delegaten korrekt verwenden kann. Dieser muss dann noch registriert werden. Nun kommt das "Vermittlungsobjekt" zum Einsatz. Es muss als Service registriert werden. Dabei ist wichtig, es im Modus "WellKnownObjectMode.Singleton" zu registrieren, damit immer nur eine einzige Instanz davon verwendet wird.

Der Rest ist einfach: Das Objekt der Activator-Klasse besorgen (an dieser Stelle wird es erzeugt, weil noch keine Instanz vorhanden ist) und den Delegaten zuweisen.

Die zweite Anwendungs-Instanz

Beim Start der zweiten Anwendungs-Instanz wird in der "program.cs" kein neuer Mutex erzeugt, da er ja schon existiert. Anstatt die Anwendung zu Starten, wird nun erneut ein IpcChannel erstellt, dieses Mal jedoch ohne das Objekt zu Serialisierung. Das wird hier nicht benötigt, da kein Delegat gesetzt wird.

Stattdessen holt man sich erneut das Vermittlungsobjekt mittels der Activator-Klasse. Dieses Mal wird der Aufruf von GetObject keine neue Instanz erzeugen, sondern die von der ersten Anwendungs-Instanz erstellte zurückgeben. Man greift also auf dasselbe Objekt wie die erste Anwendungs-Instanz zu!

Nun braucht mann nur noch den Delegaten mit dem passenden Parameter aufrufen und ist in der zweiten Anwendungs-Instanz fertig.

Eine kleine Tücke ...

... gibt es aber noch: der Delegat wird in einem anderen Thread aufgerufen, was den Zugriff auf Elemente des Formulars erschwert. Daher habe ich als Delegaten auch nicht direkt die verarbeitende Methode zugewiesen, sondern noch eine Methode drum herum gebaut: Diese sorgt mittles Invoke dafür, dass die eigentliche Methode im richtigen Thread ausgeführt wird und es zu keinen "Unfällen" kommt.


Der Test

Das oben verlinkte Demo-Projekt sollte problemlos kompilieren. Die Anwendung, die dabei herauskommt, braucht man einfach nur zweimal zu starten. Beim zweiten Start sollte keine zweite Anwendungs-Instanz erscheinen, sondern in der Listbox der ersten Anwendungs-Instanz der Programmname eingefügt werden, der ja immer in der Parmaterliste steht.




Vielen Dank an Manuel "Motzi" Pöter, welcher diesen Artikel Korrektur gelesen hat, Anregungen zur Verbesserungs des Quellcodes gab und den Teil der Serialisierung des Objektes beitrug! Danke! Danke!

Viele Grüße
Christian Stelzmann