Baukasten für verteilte Anwendungen

Veröffentlicht: 20. Apr 2002 | Aktualisiert: 20. Jun 2004

Von Michael Willers

Die Entwicklung von verteilten Anwendungen wird künftig eng mit dem Thema Web Services verbunden sein. So ist es nicht verwunderlich, dass Microsoft im .NET Framework eine komplett neue Architektur für Fernaufrufe bereitstellt.
Sie lässt sich am Besten mit einem Lego-Baukasten vergleichen: Vorgefertigte Standardkomponenten, die man flexibel einsetzen und erweitern kann, um verschiedenste Lösungen zu bauen - je nach Anforderung. Es könnte sich also lohnen, einmal genauer hinzuschauen. Aber alles der Reihe nach.

Auf dieser Seite

 Appdomains und Marshalling
 Marshalling by Value (MBV)
 Marshalling by Reference (MBR)
 Zwischenstop - Marshalling in Kurzform
 Aktivierung von fernen Objekten
 Fernaufrufe im Detail
 Proxies und Messages
 Fazit

Appdomains und Marshalling

Die Common Language Runtime (CLR) abstrahiert physikalische Betriebssystem-Prozesse und arbeitet mit virtuellen Prozessen. Diese virtuellen Prozesse werden Appdomains genannt. Durch diese Abstraktion ist die CLR unabhängig von Konzepten und der konkreten Implementierung des zugrunde liegenden Betriebssystems.

Beim Start einer .NET Anwendung wird durch die CLR implizit eine Appdomain angelegt in der die Anwendung dann läuft. Eine Appdomain kann aber auch explizit erzeugt werden. Dies ist zum Beispiel der Fall, wenn innerhalb einer Anwendung verschiedene Objekte aus Sicherheitsgründen isoliert voneinander ausgeführt werden sollen. Grundsätzlich gilt dabei die Regel:Sobald ein Aufruf eine Appdomain-Grenze überschreitet ist Marshalling notwendig.

Oder anders formuliert: Die Remoting-Mechanismen greifen. Bild 1 zeigt diesen Zusammenhang. Das Marshalling kann dabei entweder "by Value" oder "by Reference" erfolgen und es kommt die zweite,wichtige Regel ins Spiel:Marshalling ist immer mit Serialisierung und Deserialisierung verbunden.

Bild01

Bild 1: Auch auf einem Rechner und sogar innerhalb eines Prozesses findet Remoting statt!

 

Marshalling by Value (MBV)

Bei Aufrufen zwischen zwei Appdomains wird immer mit einer Kopie des Original-Objekts gearbeitet. Diese Kopie wird erzeugt, wenn das Objekt Parameter oder Rückgabewert eines Methodenaufrufs ist. Dieser Sachverhalt ist in Bild 2 dargestellt.

Bild02

Bild 2: Marshalling by Value erfolgt grundsätzlich per Objekt-Kopie.

Zu Verdeutlichung soll ein kleines C#-Programm dienen (siehe Listing 1). Hier gibt es die Klassen PersonA und PersonB, von denen Instanzen isoliert in eigenen Appdomains erstellt werden. Sie zeigen die vom .NET Framework definierten Serialisierungsmöglichkeiten:

  • PersonA:
    Ein Objekt wird mit dem Attribute [Serializable] versehen, um zu signalisieren, dass es serialisiert und deserialisiert werden kann. Dabei gibt es auch die Möglichkeit, gezielt einzelne Member des Objekts davon auszuschliessen. Sie werden mit dem Attribut [NonSerialized] gekennzeichnet. Kurz: Das Verhalten beim Serialisieren und Deserialisieren wird deklarativ festgelegt.

  • PersonB:
    Eine Klasse kann durch Ableitung von der Schnittstelle ISerializable selbst definieren, welche Member serialisiert werden sollen. Auch hier muss die Klasse aber mit dem Attribut [Serializable] versehen werden.

Allgemein: Eine Klasse muss grundsätzlich mit dem Attribut [Serializable] versehen werden. Sonst ist ein "Marshalling by Value" dieser Klasse nicht möglich! Die CLR "belohnt" einen Methodenaufruf dann mit einer Exception.

using System;
using System.Reflection;
using System.Runtime.Serialization;


namespace MBV
{
[Serializable]
class PersonA
{
private string name;
[NonSerialized] private int age;  // Alter wird nicht übertragen

public PersonA()
{
this.name = "Horst Meier";
this.age = 23;
}

public override string ToString()
{
return name + "," + age.ToString();
}
}


[Serializable]
class PersonB : ISerializable
{
// ISerializable
private PersonB(SerializationInfo info, StreamingContext ctx)
{
name = info.GetString("name");
age = info.GetInt32("age");
}
public void GetObjectData(SerializationInfo info, StreamingContext ctx)
{
Type t = this.GetType();
info.AddValue("TypeObj", t);
info.AddValue("age",0); // Alter wird nicht übertragen
info.AddValue("name",name);
}
// Person B
private string name;
private int age;

public PersonB()
{
this.name = "Horst Meier";
this.age = 23;
}

public override string ToString()
{
return name + "," + age.ToString();
}
}

class Class1
{
static void Main(string[] args)
{
string asm = Assembly.GetExecutingAssembly().ToString();
AppDomain ad = AppDomain.CreateDomain("sandbox",null,null);
// Deklarativ
PersonA pA = (PersonA)ad.CreateInstanceAndUnwrap(asm,"MBV.PersonA");
Console.WriteLine(pA);
// Imperativ
PersonB pB = (PersonB)ad.CreateInstanceAndUnwrap(asm,"MBV.PersonB");
Console.WriteLine(pB);
}
}
}

Listing 1: Das Serialisierungsverhalten von Objekten kann deklarariv oder imperativ festgelegt werden.

Bild03

Bild 3: "Marshalling by Value" illustriert.

Eine Verallgemeinerung des Marshalling-Vorgangs ist in Bild 3 dargestellt. Ein Objekt wird mit Hilfe eines Formatters in einen Stream serialisiert und auf der anderen Seite wieder deserialisiert. Auch hier bieten sich Eingriffsmöglichkeiten. Implizit benutzt das .NET Framework einen Memorystream und einen binären Formatter. Listing 2 zeigt, wie sie Objekte explizit serialisieren und deserialisieren können. Dabei werden auch Referenzen beachtet (siehe Listing 3).

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;


namespace MBV
{
[Serializable]
class Person
{
private string name;
private int age;

public Person()
{
this.name = "Horst Meier";
this.age = 23;
}

public override string ToString()
{
return name + "," + age.ToString();
}
}

class Class1
{
static void ToFile(string file, object o)
{
FileStream fs = new FileStream(file,FileMode.Create);
SoapFormatter form = new SoapFormatter();
form.Serialize(fs,o);
fs.Close();
}

static object FromFile(string file)
{
FileStream fs = new FileStream(file,FileMode.Open);
fs.Seek(0, SeekOrigin.Begin);
SoapFormatter form = new SoapFormatter();
return form.Deserialize(fs);
}

static void Main(string[] args)
{
Person p = new Person();
object[] o = { p,p,p };
int i = 0;
string file = "person.xml";
for (i = 0; i < o.Length; i++) Console.WriteLine(o[i]);
ToFile(file,o);
o = null;
o = (object[])FromFile(file);
for (i = 0; i < o.Length; i++) Console.WriteLine(o[i]);
}
}
}

Listing 2: Explizites Serialisieren und Deserialisieren von Objekten

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="https://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="https://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="https://schemas.microsoft.com/soap/encoding/clr/1.0"
SOAP-ENV:encodingStyle="https://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<SOAP-ENC:Array SOAP-ENC:arrayType="xsd:anyType[3]">
<item href="#ref-2"/>
<item href="#ref-2"/>
<item href="#ref-2"/>
</SOAP-ENC:Array>
<a2:Person id="ref-2"
xmlns:a2="https://schemas.microsoft.com/clr/nsassem/MBV/
listing2%2C%20Version%3D0.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<name id="ref-4">Horst Meier</name>
<age>23</age>
</a2:Person>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Listing 3: Beim Serialisieren werden auch Objektrefenzen berücksichtigt

Es sind standardmäßig zwei Formatter vorhanden: Ein binärer Formatter und der SOAP-Formatter. Sie haben auch die Möglichkeit, eigene Formatter zu entwickeln. Dazu müssen Sie eine Klasse erstellen, die sich von der abstrakten Basisklasse System.Runtime.Serialization.Formatter ableitet und deren Methoden überschreiben. Das ist allerdings zugegebenermaßen keine leichte Aufgabe - denken Sie beispielsweise daran, dass Ihr Formatter auch Graphen korrekt serialisieren und deserialisieren können sollte.

Für "Marshalling by Value" gilt also:

  • Die Klasse legt fest, welche Member des Objekts serialisiert werden sollen.

  • Der Formatter legt fest, wie das Objekt serialisiert werden soll.

  • Der Stream legt fest, wohin das Objekt serialiert werden soll.

 

Marshalling by Reference (MBR)

Bei Aufrufen zwischen zwei Appdomains wird mit dem Originalobjekt gearbeitet - es verbleibt immer in der Appdomain, in der es erzeugt wurde. Marshalling By Reference wird also grundsätzlich eingesetzt, wenn sich das aufgerufene Objekt auf einem anderen Rechner befindet.

Zugriff auf das Original-Objekt erhält der Aufrufer über einen Proxy, der in seiner Appdomain erstellt wird. Dieser Sachverhalt ist in Bild 4 dargestellt. Klassen, für die "Marshalling by Reference" erfolgen soll, müssen grundsätzlich von System.MarshalByRefObject abgeleitet sein!

Bild04

Bild 4: "Marshalling by Reference" erfolgt grundsätzlich per Proxy.

Der Proxy hat die Aufgabe, den Methodenaufruf in ein Messageobjekt umzuwandeln. Mit diesem Messageobjekt erfolgt dann die Serialisierung und Deserialisierung. Hier wird also ein als Objekt verpackter Methodenaufruf serialisiert und deserialisert!

Das Objekt wird in einen Stream serialisiert, der dann mit Hilfe eines Channels weitergeleitet wird. Der Channel implementiert die Protokollbindung (HTTP oder TCP) und sorgt für das Verschicken und Empfangen von Messageobjekten. Kurz: Ein Channel etabliert die Kommunikation zwischen zwei Endpunkten und transportiert Streams mit serialisierten Messages. Bild 5 zeigt diesen Sachverhalt. Wichtiger Punkt dabei: Ein Channel kennt die Details eines Endpunktes nicht.

Bild05

Bild 5: "Marshalling by Reference" illustriert.

Wenn Sie zum Beispiel einen Aufruf an die Adresse "http://www.devcoach.de:8086/MyService" schicken, sorgt der Channel dafür, dass das dazugehörige Messageobjekt über die physikalische Netzwerkverbindung dorthin geschickt wird. Listing 4 zeigt ein konkretes Beispiel in C#.

// -----------------------
// Das aufzurufende Objekt
// -----------------------
//
namespace MyServices
{
public class Service : System.MarshalByRefObject
{
public string SayHello(string strName)
{
return "Hello, " + strName + ". How are you?";
}
}
}


// -----------------------
// Der Server
// -----------------------
//
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

namespace MyServices
{
public class Server
{
public static void Main()
{
MyServices.Service svc = new MyServices.Service();
ObjRef or = RemotingServices.Marshal(svc,"MyService");
ChannelServices.RegisterChannel(new HttpChannel(8086));
Console.WriteLine("Exit with Enter");
Console.ReadLine();
}
}
}


// -----------------------
// Der Client
// -----------------------
//
using System;
using System.Runtime.Remoting;

namespace MyServiceTest
{
class Client
{
static void Main(string[] args)
{
// ACHTUNG: www.devcoach.de ist
// hier nur ein Platzhalter !!!
MyServices.Service svc = (MyServices.Service)RemotingServices.Connect(typeof(MyServices.Service),
"http://www.devcoach.de:8086/MyService");
string s = svc.SayHello("Mike");
Console.WriteLine(s);
Console.ReadLine();
}
}
}

Listing 4: Ein Channel in Aktion - Methodenaufrufe per SOAP einfach gemacht.

Für "Marshalling by Reference" gilt also:

  • Die Proxy legt fest, welche Methodenaufrufe als Messages serialisiert werden

  • Der Formatter legt fest, wie der Aufruf serialisiert werden soll

  • Der Channel legt fest, wohin der Aufruf serialiert werden soll.

 

Zwischenstop - Marshalling in Kurzform

Fassen wir an dieser Stelle das bislang Dargestellte kurz zusammen, um die Unterschiede deutlich zu machen:

  • Marshalling by Value (MBV)

    • Es wird eine Kopie des Originalobjekts erzeugt (Clone).

    • Diese Kopie wird serialisiert und deserialisiert.

    • Der Aufrufer arbeitet mit der Kopie und hat keinen Zugriff auf das Originalobjekt.

  • Marshalling by Reference (MBR)

    • Es wird ein Proxy für für das Originalobjekt erzeugt.

    • Der Proxy wandelt Methodenaufrufe in Objekte um (Messages).

    • Die Methodenaufrufe werden serialisiert und deserialisiert.

    • Der Aufrufer arbeitet mit dem Proxy und hat über ihn Zugriff auf das Originalobjekt.

Im Folgenden soll nun betrachtet werden, wie Aufrufe über Maschinengrenzen im Detail ablaufen.

 

Aktivierung von fernen Objekten

Ferne Objekte benötigen auf dem Server immer eine Anwendung, in der sie betrieben werden (Host). Bei der Aktivierung dieser Objekte gibt es zwei Möglichkeiten:

  • Objekt und zugehöriger clientseitiger Proxy werden erzeugt, sobald sie ein Client anfordert.

  • Der Proxy wird sofort erstellt, das Objekt aber erst beim ersten Methodenaufruf. Der Server kontrolliert, wann ein Objekt erstellt wird.

Im ersten Fall spricht man von Client Activated Objects (CAO). Sie sind mit dem bisherigen Verfahren in der Microsoft-Welt zur Aktivierung von serverseitigen Objekten vergleichbar. Der zweite Fall bildet von Server verwaltete Resourcen ab. Solche Objekte werden Server Activated Objects (SAO) genannt und haben in der Dokumentation des .NET Frameworks die Bezeichnung Well Known Objects. Sie können in zwei Modi betrieben werden: Als Singlecall- oder Singleton-Objekt.

Alternativ können die Objekte auch im Microsoft Webserver betrieben werden. Im Rahmen dieses Artikels werden wir uns auf das Hosting über eine separate Anwendung konzentrieren. Der interessierte Leser sei an dieser Stelle auf die Dokumentation verwiesen (Suchbegriff: "Hosting in IIS", Anführungszeichen nicht vergessen!).

Folgende Möglichkeiten kommen also in Betracht:

  • Client Activated Object (CAO)

    • Jeder Client bekommt sein eigenes Objekt.
  • Wellknown Objects

    • Singleton:

      • Es gibt nur eine einziges Objekt, auf das alle Clients Zugriff haben.

      • Somit Sharing gemeinsamer Daten möglich.

      • Konkurrierende Zugriffe auf gemeinsame Daten erfordern Synchronisation.

      • Skalierung bei vielen Clients beeinträchtigt.

    • SingleCall:

      • Bei jedem Methodenaufruf wird ein Objekt erzeugt und danach wieder abgebaut.

      • Objekt darf also keinen Status halten.

      • Somit geeignet, wenn hohe Skalierung erforderlich ist.

Um Fernaufrufe durchzuführen, müssen Sie entsprechende Konfigurationseinstellungen vornehmen. Dies können Sie entweder durch explizite Kodierung oder über Konfigurationsdateiten tun. Ersteres hat den Vorteil, das kein File-IO notwendig ist. Letzteres ist für die Praxis gedacht, in der eigentlich immer der Systemadministrator das letzte Wort hat.

Die Listings 5a uns 5b zeigen ein komplettes Beispiel in C# für die Implementierung einer Remoting-Anwendung mit serverseitig aktivierten Objekten. Die Kommunikation erfolgt dabei über einen HttpChannel - es kommt daher SOAP als Transportprotkoll zum Einsatz, weil der HttpChannel sich automatisch des SOAP-Formatters bedient. Kurz: Ein Webservice!
Die Systemkonfiguration für diesen Service können sie wie schon erwähnt per Code fest verdrahten oder in einer Konfigurationsdatei ablegen (siehe Listing 5b).

// -----------------------
// Das aufzurufende Objekt
// -----------------------
//
namespace MyServices
{
public class Service : MarshalByRefObject
{
public Service()
{
System.Console.WriteLine("MyService started");
}
public string DoSomething(string s)
{
return "I'm doing something with " + s;
}
}
}


// -----------------------
// Der Server (Host)
// -----------------------
//
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

namespace MyServices
{
class Server
{
static void ConfigureWithCode()
{
// Wir nutzen SOAP als Wire-Protokoll auf Port 8086
ChannelServices.RegisterChannel(new HttpChannel(8086));
RemotingConfiguration.RegisterWellKnownServiceType(typeof(MyServices.Service),
"MyService",WellKnownObjectMode.SingleCall);

// oder alternativ als Singleton-Objekt
// RemotingConfiguration.RegisterWellKnownServiceType(typeof(MyServices.Service),
"MyService",WellKnownObjectMode.Singleton);
}

static void ConfigureWithFile(string file)
{
RemotingConfiguration.Configure(file);
}

static void Main(string[] args)
{
ConfigureWithFile("server.cfg")
Console.WriteLine("Exit with enter");
Console.ReadLine();
}
}
}


// -----------------------
// Der Client
// -----------------------
//
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

namespace MyServiceTest
{
class Client
{
static void ConfigureWithCode
{
// ACHTUNG: www.devcoach.de ist hier nur ein Platzhalter !!!
RemotingConfiguration.RegisterWellKnownClientType(typeof(MyServices.Service),
"http://www.devcoach.de:8086/MyService");
}

static void ConfigureWithFile(string file)
{
RemotingConfiguration.Configure(file);
}

static void Main()
{
ConfigureWithFile("client.cfg")
MyServices.Service svc = new MyServices.Service();
string s = svc.DoSomething("Horst Meier");
Console.WriteLine(s);
Console.ReadLine();
}
}
}

Listing 5: Ein Webservice in C# mit einen Testclient.

<?xml version="1.0" encoding="utf-8" ?> 
<!--
server.cfg - serverseitige Konfigurationsdatei
-->
<configuration>
<system.runtime.remoting>
<application>

<service>
<wellknown mode="SingleCall" type="MyServices.Service,MyService" 
objectUri="MyService" />
<!-- <wellknown mode="Singleton" type="MyServices.Service,MyService" 
objectUri="MyService" /> -->
</service>

<channels>
<channel ref="http" port="8086" />
</channels>

</application>
</system.runtime.remoting>
</configuration>

<?xml version="1.0" encoding="utf-8" ?> 
<!--
client.cfg - clientseitige Konfigurationsdatei
-->
<configuration>
<system.runtime.remoting>
<application>

<client>
<!--
ACHTUNG: www.devcoach.de ist hier nur ein Platzhalter
-->
<wellknown type="MyServices.Service,MyService" 
url="http://www.devcoach.de:8086/MyService" />
</client>

</application>
</system.runtime.remoting>
</configuration>

Listing 5b: Die Konfiguration einer Remoting-Anwendung erfolgt üblicherweise über Konfigurationsdateien.

Soweit zu den serverseitigen Objekten. Clientseitig aktivierte Objekte lassen sich fast genauso entwickeln. Nur wer sehr genau hinschaut wird die Unterschiede im Code bemerken (siehe Listing 6a und 6b).

// -----------------------
// Das aufzurufende Objekt
// -----------------------
//
namespace MyServices
{
public class Service : MarshalByRefObject
{
public Service()
{
System.Console.WriteLine("MyService started");
}
public string DoSomething(string s)
{
return "I'm doing something with " + s;
}
}
}

// -----------------------
// Der Server (Host)
// -----------------------
//
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

namespace MyServices
{
class Server
{
static void ConfigureWithCode()
{
RemotingConfiguration.ApplicationName = "MyService";
RemotingConfiguration.RegisterActivatedServiceType(typeof(MyServices.Service));
// Wir nutzen SOAP als Wire-Protokoll auf Port 8086
ChannelServices.RegisterChannel(new HttpChannel(8086));
}

static void ConfigureWithFile(string file)
{
RemotingConfiguration.Configure(file);
}

static void Main()
{
ConfigureWithFile("server.cfg");
Console.WriteLine("Exit with enter");
Console.ReadLine();
}
}
}

// -----------------------
// Der Client
// -----------------------
//
using System;
using System.Runtime.Remoting;

namespace MyServiceTest
{
class Client
{
static void ConfigureWithCode()
{
// ACHTUNG: www.devcoach.de ist hier nur ein Platzhalter !!!
RemotingConfiguration.RegisterActivatedClientType(typeof(MyServices.Service),
"http://www.devcoach.de:8086/MyService");
}

static void ConfigureWithFile(string file)
{
RemotingConfiguration.Configure(file);
}

static void Main()
{
ConfigureWithFile("client.cfg");
MyServices.Service svc = new MyServices.Service();
string s = svc.DoSomething("Horst Meier");
Console.WriteLine(s);
Console.ReadLine();
}
}
}

Listing 6a: Ein clientseitig aktiviertes Objekt mit Testprogramm.

<?xml version="1.0" encoding="utf-8" ?> 
<!--
server.cfg - serverseitige Konfigurationsdatei
-->
<configuration>
<system.runtime.remoting>
<application name="MyService">

<service>
<activated type="MyServices.Service,Services"/>
</service>

<channels>
<channel ref="http" port="8086" />
</channels>

</application>
</system.runtime.remoting>
</configuration>

<?xml version="1.0" encoding="utf-8" ?> 
<!--
client.cfg - clientseitige Konfigurationsdatei
-->
<configuration>
<system.runtime.remoting>
<application>
<!--
ACHTUNG: www.devcoach.de ist nur ein Platzhalter
-->
<client url="http://www.devcoach.de:8086/MyService">
        <activated type="MyServices.Service,Services"/>
</client>

</application>
</system.runtime.remoting>
</configuration>

Listing 6b: Auch clientseitig aktivierte Objekte werden über Konfigurationsdateien eingerichtet.

 

Fernaufrufe im Detail

Die Bilder 6 und 7 zeigen, wie Methodenaufrufe in .NET Remoting im Detail ablaufen. Zunächst erzeugt die Common Laguage Runtime den sogenannten RealProxy. Er ist für die Umwandlung eines Methodenaufrufs in ein Messageobjekt zuständig. Damit der aufrufende Client es aber möglichst einfach hat, erzeugt der RealProxy aus den Metadaten einen Transparent Proxy, der die gleiche Funktionalität bietet, wie das entfernte Objekt. Der Client spricht nun direkt mit dem Transparent Proxy. Deshalb bildet er auch den ersten Schritt im Flussdiagramm in Bild 6.

Das so erzeugte Messageobjekt wird nun durch einen oder mehrere Messagesinks geleitet, in denen es noch verändert werden kann. Der nächste Schritt ist dann der Client Formatter Sink. An dieser Stelle wird dann das Messageobjekt mit Hilfe eines Formatters (siehe Abschnitt Marshalling) in einen Stream serialisiert. Der Stream kann dann über einen oder mehrere Channelsinks "laufen", bevor der Aufruf endgültig den Clientrechner verlässt und mit Hilfe eines Channels über "den Draht" zum Server geschickt wird.
Channelsinks bieten zum Beispiel die Möglichkeit, Aufrufe zu protokollieren und sind deutlich einfacher zu implementieren als ein kompletter Channel. Serverseitig läuft dieses Verfahren dann in umgekehrter Reihenfolge ab.

Bild06

Bild 6: Methodenaufrufe in .NET Remoting im Detail - die Clientseite.

Bild07

Bild 7: Methodenaufrufe in .NET Remoting im Detail - die Serverseite.

Sollte die Basisfunktionalität nicht ausreichen, bietet das .NET Framework somit folgende Stellen, an denen man eigene Funktionalität "einklinken" kann:

  • In die Generierung von Messages durch Implementieren eines eigenen Proxies.

  • In die Weiterleitung von Messages durch Implementieren von Messagesinks (Dies erfolgt über Kontextattribute.).

  • In die Serialisierung einer Message in einen Stream durch Implementieren eines Formatters.

  • In die Weiterleitung von Streams durch Implementieren von Channelsinks.

  • In die Protokollbindung durch Implementieren eines Channels.

 

Proxies und Messages

Abschließend soll nun dargestellt werden, wie man in die Generierung von Messages durch Implementieren eines eigenen Proxies eingreifen kann. Die weiteren Punkte können hier aus Platzgründen leider nicht aufgenommen werden. Sie bieten genug Stoff für eigene Artikel.

Insbesondere das Implementieren einer Protokollbindung über einen selbstdefinierten Channel bedarf eines eigenen Artikels. Selbiges gilt für die Implementierung von Messagesinks über Kontextattribute. Bezüglich der Channelsinks möchte ich auf die Beispielprogramme des .NET Framework SDKs verweisen. Hier finden Sie im Verzeichnis \Samples\Technologies\Remoting\Advanced\ChannelSinks\Logging\ ein komplettes Beispiel, das Methodenaufrufe über einen eigenen Channelsink protokolliert.

Die Messageobjekte, die der Proxy generiert, werden im .NET Framework durch die Schnittstellen IMessage und IMethodMessage sowie Ihre Ableitungen definiert. Auf diese Weise kann man innerhalb des Proxies immer unterscheiden, um welche Message sich handelt. Listing 7 zeigt den Aufbau beider Schnittstellen. Insgesamt gibt es folgende Ableitungen:

  • IMethodCallMessage - eine Methode wird aufgerufen.

  • IMethodReturnMessage - ein Methodenaufruf kehrt zurück.

  • IConstructionCallMessage - ein Konstruktor wird aufgerufen.

  • IConstructionReturnMessage - ein Konstruktoraufruf kehrt zurück.

namespace System.Runtime.Remoting.Messaging
{
public interface IMessage
{
IDictionary Properties { get; }
}

public interface IMethodMessage : IMessage
{
string Uri { get; }
string MethodName { get; }
string TypeName { get; }
object MethodSignature { get; }
int ArgCount { get; }
string GetArgName(int index);
object GetArg(int index);
object[] Args { get; }
bool hasVarArgs { get; }
LogicalCallContext LogicalCallContext { get; }
System.Reflection.MethodBase MethodBase { get; }
}
}

Listing 7: Die Schnittstellen IMessage und IMethodMessage im Detail

Um einen eigenen Proxy zu erzeugen, müssen Sie eine Klasse anlegen, die sich von der abstrakten Basisklasse System.Runtime.Remoting.Proxies.RealProxy ableitet und deren Methode Invoke überschreiben. Ein in der Praxis häufig benutzter Fall dürfte die Protokollierung von Methodenaufrufen zur Fehlersuche sein (Listing 8).

using System;
using System.Collections;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Messaging;

namespace ProxySpy
{
// Proxyklasse
public class MyProxy : RealProxy
{
MarshalByRefObject target;

public MyProxy(Type t) : base(t)
{
target = (MarshalByRefObject)Activator.CreateInstance(t);
}

public override IMessage Invoke(IMessage msg)
{
Console.WriteLine("MyProxy.Invoke Start\n");
Console.WriteLine("Message Properties\n");
   // Inhalt der Message ausgeben            
IDictionary d = msg.Properties;
IDictionaryEnumerator e = (IDictionaryEnumerator) d.GetEnumerator();
while (e.MoveNext())
        {
string name = e.Key.ToString();
object val = e.Value;
Console.WriteLine("\t{0} : {1}", name, val);
if ((name == "__Args")  && (null != val))
               {
object[] args = (object[])val;
                  foreach (object arg in args) Console.WriteLine(arg);
               }
               if ((name == "__MethodSignature") && (null != val))
{
   object[] args = (object[])val;
   foreach (object arg in args) Console.WriteLine(arg);
}
}

// Methode ausführen
IMethodCallMessage call = (IMethodCallMessage)msg;
IMessage response = RemotingServices.ExecuteMessage(target, call);

Console.WriteLine("\nMyProxy.Invoke - Finish");
return response;
        }
}


// Klasse für die Metadaten
public class MyClass : MarshalByRefObject
{
public int MyMethod(String s, double d, int i)
{
Console.WriteLine("MyMethod {0} {1} {2}", s, d, i);
return 42;
}
}


// Hauptprogramm
    public class EntryPoint
    {
public static MyClass GetObj()
{
MyProxy myProxy = new MyProxy(typeof(MyClass));
return (MyClass)myProxy.GetTransparentProxy();
}

public static void Main()
{
MyProxy myProxy = new MyProxy(typeof(MyClass));
MyClass c = (MyClass)myProxy.GetTransparentProxy();
int i = c.MyMethod("String1", 1.2, 6);
Console.WriteLine(i);
}
}
}

Listing 8: Ein eigener Proxy protokolliert Methodenaufrufe

 

Fazit

Soweit ein erster Rundgang durch die Remotingarchitektur im .NET Framework. Sie bildet eine solide Basis, die sich durch Einklinken eigener Funktionalität an spezielle Problemstellungen anpassen und erweitern lässt. Diese Eingriffsmöglichkeiten sind so umfassend, dass sie mit Ausnahme des Proxies nur angerissen aber nicht detailliert betrachtet werden konnten. Der wichtigste Punkt dabei: Die dazu notwendigen Schnittstellen sind dokumentiert und komplett offengelegt.

Und nicht zuletzt basiert die Kommunikation nicht mehr auf einem eigenenProtokoll, sondern setzt auf vorhandene Internet-Standards, wie HTTP und XML, auf. Damit ist ein weiterer Schritt in Richtung Anwendungsintegration über Plattformgrenzen hinweg getan. Es bleibt also spannend.