MSDN Magazin > Home > Ausgaben > 2008 > Juni >  Service Station: Erstellen eines WCF-Routers, T...
Service Station
Erstellen eines WCF-Routers, Teil 2
Michele Leroux Bustamante

Der Code kann hier heruntergeladen werden: ServiceStation2008_06.exe (340 KB)
Code online durchsuchen

In der Ausgabe vom April 2008 von Service Station habe ich Ihnen gezeigt, wie ein einfacher Router erstellt werden kann, der transparenten Nachrichtenfluss zwischen dem aufrufenden Client und dem Zieldienst ermöglicht. Dabei habe ich wichtige Adressierungs- und Nachrichtenfilterungssemantik von Windows® Communication Foundation (WCF) behandelt. Sie haben gelernt, wie ein Routervertrag entworfen wird, der mit nicht typisierten Nachrichten funktioniert, und wie die Bindungen und Verhaltensweisen konfiguriert werden, die es Nachrichten ermöglichen, vom Router unberührt zu passieren. In dieser Ausgabe setze ich die Diskussion fort, indem ich auf weitere Implementierungsdetails eingehe, die sich beim Einsatz praktischerer Szenarios für Router ergeben.

Pass-Through-Router-Szenarios
Wie bereits in Teil 1 erwähnt, unterhält der Client, wenn ein Pass-Through-Router zwischen einem Client und einem Dienst eingefügt wird, eine Beziehung zum Zieldienst, nicht zum Router. Obwohl Nachrichten natürlich mit einem Transportprotokoll und Nachrichtencodierer gesendet werden müssen, die der Router verstehen kann, wird der gesamte Inhalt der Nachricht (z. B. einschließlich Sicherheitsheader und zuverlässiger Sitzungen) nicht vom Router verarbeitet. Beispiele für Szenarios, in denen ein Pass-Through-Router eingesetzt werden könnte, sind Lastenausgleich, inhaltsbasiertes Routing oder Nachrichtentransformation.
Lastenausgleich und Arbeitsverteilung über Serverressourcen eignen sich am besten für Netzwerklastenausgleich (Network Load Balancing, NLB) oder besser noch für Hardwaregeräte für den Lastenausgleich. Ein WCF-Router kann nützlich sein für den Lastenausgleich, wenn Dienste in einer Umgebung ohne diese Möglichkeiten gehostet werden, wenn Dienste in einer physischen Infrastruktur installiert sind, über die Sie keine direkte Kontrolle haben, wenn Sie Routing auf Basis domänenspezifischer Logik benötigen oder wenn die Anwendung schlicht nach einer einfachen Routinglösung verlangt, die sich leicht konfigurieren lässt. Ein solcher WCF-Router kann zur Verteilung von Nachrichten an Dienste verwendet werden, die in mehreren Prozessen auf dem gleichen Computer oder auf verschiedenen Computern gehostet werden.
Unabhängig vom Verteilungsmodell erfordert ein Router für den Lastenausgleich allerdings einige zentrale Features. Dienste müssen im Router registriert werden können, damit sie in die Lastverteilung aufgenommen werden. Der Router muss in der Lage sein, den Diensttyp und den dazugehörigen Endpunkt zu bestimmen, damit Nachrichten korrekt weitergeleitet werden können. Außerdem muss der Router über einen Algorithmus für die Verteilung der Last verfügen, z. B. einen klassischen Roundrobin-Ansatz oder eine Form des prioritätenbasierten Routing.
In manchen Fällen wird die Verteilung der Nachrichten unter Diensten auf Basis des Inhalts der Nachricht statt des Lastenausgleichs geregelt. Ein inhaltsbasierter Router überprüft in der Regel entweder den Nachrichtenkopf oder den Nachrichtentext auf Routinginformationen. So ist es beispielsweise möglich, dass Nachrichten von Clients mit einem gültigen Lizenzschlüssel als Nachrichten mit hoher Priorität an einen großen Pool von Servercomputern mit höherer Verarbeitungsleistung weitergeleitet werden, während Nachrichten von Clients mit einer Evaluierungslizenz an einen kleineren Pool mit weniger leistungsfähigen Servern weitergeleitet werden. In diesem Szenario muss der Router nicht nur wissen, wohin die Nachrichten weiterzuleiten sind, sondern außerdem in der Lage sein, den Kopf oder Text jeder Nachricht zu überprüfen, bevor er die Entscheidung über das Ziel der Weiterleitung trifft. In den folgenden Abschnitten werden relevante Routingfeatures behandelt, die diese Szenarios unterstützen.

Weiterleitung durch Action-Header
Am Router eingehende Nachrichten haben zwei Adressierungsheader, die für die Weiterleitung von Nachrichten an den richtigen Dienst nützlich sein können:
Der To-Header gibt den Namen des Endpunkts an. Wenn der Header dem Zieldienst und nicht dem Router entspricht, gibt er eine URL für den Dienstendpunkt an, für den die Nachricht bestimmt war.
Der Action-Header gibt den Dienstvorgang an, für den die Nachricht bestimmt war, stellt jedoch nicht unbedingt eine gültige URL dar.
In vielen Fällen entspricht jedoch der To-Header der Routeradresse und nicht dem Dienst. Somit ist der Action-Header eine zuverlässigere Informationsquelle im Hinblick auf das richtige Ziel für die Nachricht. Sicher erinnern Sie sich, dass der Action-Header vom Dienstvertragsnamespace, Dienstvertragsnamen und dem Vorgangsnamen abgeleitet ist. Damit stehen dem Router ausreichend Informationen zur eindeutigen Identifizierung des Zieldiensts zur Verfügung, sofern Verträge nicht über verschiedene Diensttypen hinweg verteilt sind. Betrachten Sie die folgenden Dienstverträge, alle in verschiedenen Diensttypen implementiert:
[ServiceContract(Namespace = 
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceA {
  [OperationContract]
  string SendMessage(string msg);
}
[ServiceContract(Namespace = 
  "http://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceB {
  [OperationContract]
  string SendMessage(string msg);
}
public class ServiceA : IServiceA {...}
public class ServiceB : IServiceB{...}
Wie Abbildung 1 zeigt, kann sich der Router auf eine Zuordnung zwischen Vertragsnamespaces für jeden Dienstvertrag und die Dienstendpunkte stützen, an die Nachrichten gesendet werden sollen.
Abbildung 1 Zuordnung von Vertragsnamespace zu Dienstendpunkt (Klicken Sie auf das Bild, um es zu vergrößern)
Im Folgenden wird ein Wörterbuch gezeigt, das so initialisiert wurde, dass jeder Vertragsnamespace-Eintrag einem Konfigurationselement zugeordnet ist, das die für die Kanalkonfiguration zu verwendenden Einstellungen angibt:
static public IDictionary<string, string> RegistrationList = 
  new Dictionary<string, string>();

RegistrationList.Add(
  "http://www.thatindigogirl.com/samples/2008/01/IServiceA", 
  "ServiceA");
RegistrationList.Add(
  "http://www.thatindigogirl.com/samples/2008/01/IServiceB", 
  "ServiceB");
Der Code für die Initialisierung des Kanals sähe dann wie folgt aus:
string contractNamespace = 
  requestMessage.Headers.Action.Substring(0, 
  requestMessage.Headers.Action.LastIndexOf("/"));

string configurationName = 
  RouterService.RegistrationList[contractNamespace];

using (ChannelFactory<IRouterService> factory = 
  new ChannelFactory<IRouterService>(configurationName))
{...}
In diesem Szenario gibt es einige wichtige Entwurfsabhängigkeiten, die Sie beachten sollten:
  • Die Zuordnung von Verträgen zu Diensten wird höchstwahrscheinlich in einer Datenbank gespeichert, um die Konfiguration zu vereinfachen und mehrere Routerinstanzen zu unterstützen.
  • Der Dienstvertrag kann nicht in mehreren Diensttypen implementiert werden, es sei denn, Nachrichten können von jedem Dienst verarbeitet werden, der den Vertrag implementiert.
  • Wenn mehrere Instanzen eines Diensts in einer Serverfarm vorhanden sind, sollte die Konfiguration für jeden Endpunkt einer virtuellen Adresse zugeordnet sein, die die physischen Lastverteiler dann dementsprechend verteilen können.
  • Es gibt keine Unterstützung für Nachrichten, die einen Action-Header enthalten, mit Ausnahme derer für Anwendungsdienste.
Dieser letzte Punkt ist wichtig, denn wenn sichere Sitzungen oder zuverlässige Sitzungen für Anwendungsdienste aktiviert sind, werden vor den eigentlichen Anwendungsdienstnachrichten zusätzliche Nachrichten gesendet, um diese Sitzungen einzurichten. Diese Nachrichten verwenden einen Action-Header für ihre jeweiligen Protokolle und sind vollständig unabhängig von Anwendungsdiensten. Dies bedeutet, dass zur Nachrichtenweiterleitung eine Alternative zum Action-Header verwendet werden muss.

Weiterleitung mit benutzerdefinierten Headern
Um sicherzustellen, dass jede Nachricht einen Routingheader enthält, der den Anwendungsdienst angeben kann, mit dem der Client zu kommunizieren versucht, kann im Abschnitt zur Endpunktkonfiguration für den Anwendungsdienst wie hier gezeigt ein benutzerdefinierter Header angegeben werden:
<service behaviorConfiguration="serviceBehavior" 
  name="MessageManager.ServiceA">
  <endpoint address="http://localhost:8010/RouterService"
    binding="wsHttpBinding" bindingConfiguration="wsHttp" 
    contract="IServiceA" listenUri="ServiceA">
    <headers>
      <Route 
        xmlns="http://www.thatindigogirl.com/samples/2008/01">
        http://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</service>
Benutzerdefinierte Header besitzen einen Namen, einen Namespace und einen Wert. In einigen Fällen sind Header dynamischer, in diesem Fall jedoch ist der Header fixiert, um den Dienstvertragsnamespace darzustellen. Das Route-Element gibt den Headernamen an, und der Namespace wird vom xmlns-Attribut angegeben. Da dieser Header als Teil der Endpunktkonfiguration angegeben wird, ist er in den Metadaten für den Dienst enthalten. Wenn Clients einen Proxy generieren, generieren sie auch eine Clientkonfiguration, die den Header enthält, wie hier gezeigt:
<client>
  <endpoint address="http://localhost:8010/RouterService" 
    binding="wsHttpBinding" bindingConfiguration="wsHttp"
    contract="localhost.IServiceA" >
    <headers>
      <Route xmlns="http://www.thatindigogirl.com/samples/2008/01">
        http://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</client>
Dies macht die Anwesenheit des Headers für den Clientcodierungsaufwand transparent und stellt sicher, dass alle Nachrichten (einschließlich derer zur Einrichtung sicherer Sitzungen oder zuverlässiger Sitzungen) den Header enthalten. Der Router kann den Headerwert aus jeder Nachricht anhand des Namens und Namespace abrufen, wie im Folgenden gezeigt:
string contractNamespace = 
  requestMessage.Headers.GetHeader<string>(
  "Route", 
  "http://www.thatindigogirl.com/samples/2008/01");
Die einzige Änderung dieser Implementierung im Vergleich zum vorherigen Beispiel besteht darin, wie der Router den Vertragsnamespace erkennt: mithilfe des benutzerdefinierten Route-Headers statt des Action-Headers. Dies ermöglicht es dem Router, Nachrichten in Verbindung mit sicheren Sitzungen oder zuverlässigen Sitzungen an den richtigen Dienstendpunkt weiterzuleiten.

Registrieren von Diensten
Statt Endpunkte für Anwendungsdienste hartzucodieren, kann der Router ganz nach Bedarf einen Dienstendpunkt für die Registrierung von Diensten verfügbar machen und die Registrierung bei Bedarf wieder aufheben. Da weder Software noch Hardware zum Lastenausgleich verfügbar sind, verringert dies den Konfigurationsaufwand des Routers, wenn die Anwendungsdienste ausskaliert werden müssen oder wenn sich Port- oder Computernamen mit ihren jeweiligen Endpunktadressen ändern. Zur Unterstützung dieses Modells können die folgenden Schritte ausgeführt werden:
  • Implementieren Sie einen Dienstregistrierungsvertrag für den Router, und machen Sie diesen Endpunkt für Anwendungsdienste hinter der Firewall verfügbar.
  • Führen Sie eine Registrierungsliste für den Router.
  • Weisen Sie jeden ServiceHost nach dessen Initialisierung an, Dienstendpunkte beim Router zu registrieren.
  • Heben Sie die Registrierung beim Router auf, sobald bei einem ServiceHost ein Fehler auftritt oder ein ServiceHost geschlossen wird.
Das Diagramm in Abbildung 2 zeigt den Registrierungsprozess. Hierbei werden Einträge hinzugefügt, die den Vertragsnamespace enthalten, der einer physischen Endpunktadresse zugeordnet ist.
Abbildung 2 Registrieren von Diensten beim Router (Klicken Sie auf das Bild, um es zu vergrößern)
Bei Verwendung dieses Ansatzes sind für die Registrierung nur ein Vertragsnamespace und eine physische Adresse für jeden Dienstendpunkt erforderlich. Abbildung 3 zeigt den IRegistrationService-Dienstvertrag und die dazugehörigen RegistrationInfo-Details, die zur Registrierung und zum Aufheben der Registrierung an den Router weitergegeben werden.
[ServiceContract(Namespace = 
  "http://www.thatindigogirl.com/samples/2008/01")]
public interface IRegistrationService {
  [OperationContract]
  void Register(RegistrationInfo regInfo);

  [OperationContract]
  void Unregister(RegistrationInfo regInfo);
}

[DataContract(Namespace = 
  "http://schemas.thatindigogirl.com/samples/2008/01")]
public class RegistrationInfo {
  [DataMember(IsRequired = true, Order = 1)]
  public string Address { get; set; }

  [DataMember(IsRequired = true, Order = 2)]
  public string ContractName { get; set; }

  [DataMember(IsRequired = true, Order = 3)]
  public string ContractNamespace { get; set; }

  public override int GetHashCode()   {
    return this.Address.GetHashCode() + 
    this.ContractName.GetHashCode() + 
    this.ContractNamespace.GetHashCode();
  }
}
Der Router könnte einen einzelnen Eintrag pro Vertrag speichern. Dies würde jedoch bedeuten, dass für jeden Vertrag nur ein Dienst möglich ist. Um die Verteilung über mehrere Einträge zu unterstützen, muss der Router pro Registrierung einen eindeutigen Schlüssel verwenden. In diesem Code wird ein Wörterbuch verwendet, das jeden Eintrag eindeutig einem Hashcode für die RegistrationInfo-Instanz zuordnet:
// registration list
static public IDictionary<int, RegistrationInfo> 
  RegistrationList = 
  new Dictionary<int, RegistrationInfo>();

// to register
if (!RouterService.RegistrationList.ContainsKey(
  regInfo.GetHashCode())) {
  RouterService.RegistrationList.Add(regInfo.GetHashCode(), 
    regInfo);
  }

  // to unregister
  if (RouterService.RegistrationList.ContainsKey(
    regInfo.GetHashCode())) {
    RouterService.RegistrationList.Remove(
      regInfo.GetHashCode());
  }
Wenn der Router Nachrichten empfängt, sollte er den Vertragsnamespace erfassen, nach einem passenden Element im Wörterbuch suchen, und wenn mehr als eines existiert, die Nachricht mithilfe von Auswahlkriterien an einen geeigneten Dienstendpunkt weiterleiten (siehe Abbildung 4).
string contractNamespace = 
  requestMessage.Headers.Action.Substring(0, 
  requestMessage.Headers.Action.LastIndexOf("/"));

// get a list of all registered service entries for 
// the specified contract
var results = from item in RouterService.RegistrationList
  where item.Value.ContractNamespace.Contains(contractNamespace)
  select item;

int index = 0;
// find the next address used ...

// create the channel 
RegistrationInfo regInfo = results.ElementAt<KeyValuePair<int, 
  RegistrationInfo>>(index).Value;

Uri addressUri = new Uri(regInfo.Address);
Binding binding = ConfigurationUtility.GetRouterBinding    (addressUri.Scheme);
EndpointAddress endpointAddress = new EndpointAddress(regInfo.Address);

ChannelFactory<IRouterService> factory = new 
  ChannelFactory<IRouterService>(binding, endpointAddress)
// forward message to the service ...
Die dynamische Registrierung eignet sich nicht nur für den Lastenausgleich von Diensten über mehrere Computer hinweg, sondern ist außerdem sehr nützlich in Szenarios, in denen mehrere Instanzen eines Diensts auf demselben Computer gehostet werden. In einem solchen Fall sind mehrere Portzuweisungen erforderlich, wenn die Instanzen in einem Windows-Dienst gehostet werden.
Um dies zu unterstützen, müssen Dienste eine dynamische Portzuweisung für den Computer auswählen. Für TCP-Dienste kann dies erreicht werden, indem für den Abhör-URI-Modus in der Endpunktkonfiguration der Wert „Unique“ festgelegt wird:
<endpoint address="net.tcp://localhost:9000/ServiceA" 
  contract=" IServiceA" binding="netTcpBinding"
  listenUriMode="Unique"/>
Für Named Pipes und HTTP wird bei dieser Einstellung jedoch kein eindeutiger Port ausgewählt. Stattdessen wird eine GUID an die Adresse angehängt:
net.tcp://localhost:64544/ServiceA
http://localhost:8000/ServiceA/66e9c367-b681-4e4f-8d12-80a631b7bc9b
net.pipe://localhost/ServiceA/6660c07e-c9f5-450b-8d40-693ad1a71c6e
Um sicherzustellen, dass für TCP- und HTTP-Dienstendpunkte ein eindeutiger Port existiert, können Sie Basisadressen oder explizite Endpunktadressen im Code initialisieren:
Uri httpBase = new Uri(string.Format(
  "http://localhost:{0}", 
  FindFreePort()));
Uri tcpBase = new Uri(string.Format(
  "net.tcp://localhost:{0}", 
  FindFreePort()));
Uri netPipeBase = new Uri(string.Format(
  "net.pipe://localhost/{0}", 
  Guid.NewGuid().ToString()));

ServiceHost host = new ServiceHost(typeof(ServiceA), 
  httpBase, tcpBase, netPipeBase);
Abbildung 5 zeigt die Registrierung mehrerer Dienste, die auf demselben Computer gehostet werden, beim Router. Außerdem zeigt diese Abbildung, dass unter Umständen eine Software für den Lastenausgleich oder ein physischer Lastenausgleich zur Verteilung von Registrierungsaufrufen zwischen Instanzen erforderlich ist, um die einzelne Fehlerquelle des Routers zu entfernen. Selbstverständlich impliziert dies auch, dass die Registrierungsliste in einer freigegebenen Datenbank gespeichert wird.
Abbildung 5 Registrieren von Diensten mit dynamischen Ports durch einen Router mit Lastenausgleich (Klicken Sie auf das Bild, um es zu vergrößern)

Überprüfen von Nachrichten
Obwohl Router in der Regel die ursprüngliche Nachricht an Anwendungsdienste weiterleiten, führen sie u. U. Aktivitäten basierend auf dem Inhalt der Nachricht aus. Dazu gehören z. B. das Überprüfen von Headern oder Textelementen auf inhaltsbasiertes Routing oder die Ablehnung von Nachrichten je nach Gültigkeit von Headern oder Textelementen.
Die Überprüfung von Headern ist einfach, da der Message-Typ eine Header-Eigenschaft verfügbar macht, um Adressierungsheader direkt und benutzerdefinierte Header anhand ihrer Namen und Namespaces abzurufen. Betrachten Sie den folgenden Dienstvorgang, der mithilfe eines Nachrichtenvertrags einen benutzerdefinierten LicenseKey-Header für den eingehenden Vorgang hinzufügt:
// operation
[OperationContract]
SendMessageResponse SendMessage(SendMessageRequest message);

// message contract
[MessageContract]
public class SendMessageRequest {
  [MessageHeader]
  public string LicenseKey { get; set; }

  [MessageBodyMember]
  public string Message { get; set; }
}
Clients senden Nachrichten einschließlich eines LicenseKey-Headers (vielleicht leer), wenn sie noch nicht über einen Lizenzschlüssel verfügen. Der Router kann diesen Header folgendermaßen abrufen:
string licenseKey = 
  requestMessage.Headers.GetHeader<string>(
  "LicenseKey", 
  "http://www.thatindigogirl.com/samples/2008/01");
Wenn derselbe LicenseKey-Wert innerhalb des Nachrichtentexts weitergegeben würde, müsste der Router den Nachrichtentext lesen, um auf den Wert zuzugreifen (da diese Informationen nicht direkt über den Message-Typ verfügbar sind). Die GetReaderAtBodyContents-Methode gibt einen „XmlDictionaryReader“ zurück, der wie folgt verwendet werden kann, um den Nachrichtentext zu lesen:
XmlDictionaryReader bodyReader = 
  requestMessage.GetReaderAtBodyContents();
Die State-Eigenschaft der Message kann einer der folgenden MessageType-Enumerationswerte sein: Created, Copied, Read, Written oder Closed. Die Nachricht beginnt im Created-Status. Da Router, die die Message-Parameter für Vorgänge erhalten, keine Nachrichten verarbeiten, wird der Status „Created“ beibehalten.
Das Lesen des Nachrichtentexts führt dazu, dass die Anforderungsnachricht vom Status „Created“ in den Status „Read“ versetzt wird. Nach dem Lesen kann sie nicht an Anwendungsdienste weitergeleitet werden, weil die Nachricht nur einmal gelesen oder einmal kopiert und weil nur einmal in die Nachricht geschrieben werden kann.
Vor dem Lesen sollte die Nachricht von einer inhaltsbasierten Routerimplementierung in einen Puffer kopiert werden. Mithilfe dieser zwischengespeicherten Kopie der Nachricht können neue Kopien der ursprünglichen Nachricht erstellt und wie folgt für die Verarbeitung verwendet werden:
MessageBuffer messageBuffer = 
  requestMessage.CreateBufferedCopy(int.MaxValue);
Message messageCopy = messageBuffer.CreateMessage();
XmlDictionaryReader bodyReader = 
  messageCopy.GetReaderAtBodyContents();

XmlDocument doc = new XmlDocument();
doc.Load(bodyReader);
XmlNodeList elements = doc.GetElementsByTagName("LicenseKey");
string licenseKey = elements[0].InnerText;
Derselbe Puffer kann erneut verwendet werden, um eine Nachricht für die Weiterleitung an Anwendungsdienste zu erstellen. Der Aufruf von „CreateMessage“ gibt eine neue Message-Instanz auf Basis der ursprünglichen Nachricht zurück.

Router und Transportsitzungen
In einer Pass-Through-Router-Situation müssen Clients Nachrichten mithilfe des vom Router erwarteten Transportprotokolls und Codierungsformats senden, und der Router muss zur Weiterleitung der Nachricht an Anwendungsdienste das Transportprotokoll und Codierungsformat verwenden, das von den Anwendungsdiensten erwartet wird. Alle bisher besprochenen Routingfeatures funktionieren hervorragend, wenn an beiden Enden HTTP eingesetzt wird – mit oder ohne Sitzungen. Wenn Sie jedoch eine Transportsitzung wie TCP einführen, ergeben sich einige interessante Herausforderungen. Im einfachsten Fall, d. h. bei Deaktivierung der Sicherheit und Abwesenheit zuverlässiger Sitzungen, treten keine Probleme auf. Interessant wird es erst, wenn diese Features hinzukommen.
Nach dem Deaktivieren der Sicherheit für den Anwendungsdienst muss der Router einen signierten To-Header bereitstellen. Normalerweise bedeutet dies, dass der To-Header unverändert bleibt (wie er vom Client gesendet wurde), der Router passt den To-Header jedoch standardmäßig an die Adresse des Diensts an, wenn Nachrichten gesendet werden, sofern nicht die manuelle Adressierung aktiviert ist. Wenn der Router beispielsweise das TCP-Protokoll für die Weiterleitung von Nachrichten an einen Dienst verwendet, ist die manuelle Adressierung nicht zulässig, wenn der ausgehende Kanal auf einem Anforderung-Antwort-Vertrag basiert.
Ein anderes Problem taucht auf, wenn zuverlässige Sitzungen aktiviert sind und der Router das TCP-Protokoll verwendet, um den Dienst aufzurufen. In diesem Fall werden asynchrone Bestätigungen durch den Router zurückgeschickt. Hierzu ist es erforderlich, dass der Router eine Sitzung mit dem Dienst aufrechterhält, um diese asynchronen Bestätigungen empfangen zu können. Folglich muss der Client eine Duplexsitzung mit dem Router unterhalten, um dieselben asynchronen Bestätigungen zu empfangen.
Beide Probleme lassen sich teilweise durch Implementierung eines Routers lösen, der Sitzungen unterstützt und eingehende und ausgehende Duplexkanäle verwendet. Weder der aufrufende Client noch der Anwendungsdienst müssen dies direkt zur Kenntnis nehmen. Es handelt sich um ein Implementierungsdetail innerhalb des Routers. Allerdings besteht eine Abhängigkeit von Bindungen, die Sitzungen erkennen, und von Duplexkommunikation, wenn asynchrone Bestätigungen für zuverlässige Sitzungen eingeführt werden.

Duplexrouter
Der Code in Abbildung 6 zeigt ein Beispiel für einen Duplexroutervertrag zur Unterstützung des Szenarios, in dem Nachrichten zwischen Client, Router und Anwendungsdiensten über TCP gesendet werden. Folgende Unterschiede bestehen zum traditionellen Anforderung-Antwort-Routervertrag:
  • „ProcessMessage“ ist jetzt ein unidirektionaler Vorgang.
  • Der Dienstvertrag erfordert Sitzungen. Außerdem ist ihm ein Rückrufvertrag zugeordnet. Beachten Sie bitte unbedingt, dass der Client hierfür keinen Rückruf implementieren muss. Dies ist ein interner Routervorgang.
  • Der Rückrufvertrag verfügt über eine einzelne unidirektionale Methode, um Antworten auf Routeraufrufe an Anwendungsdienste zu empfangen. Beachten Sie außerdem, dass Dienste nicht erkennen, dass ihre Antworten an einen Rückrufkanal gesendet werden. Es kann sich um Anforderung-Antwort-Nachrichten handeln.
[ServiceContract(Namespace = 
  "http://www.thatindigogirl.com/samples/2008/01", 
  SessionMode = SessionMode.Required, 
  CallbackContract = typeof(IDuplexRouterCallback))]

public interface IDuplexRouterService {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}

[ServiceContract(Namespace = 
  "http://www.thatindigogirl.com/samples/2008/01", 
  SessionMode = SessionMode.Allowed)]
public interface IDuplexRouterCallback {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}
Die Architektur für einen Duplexrouter wird in Abbildung 7 gezeigt. Was den Client angeht, werden Anforderungen gesendet und eine synchrone Antwort wird erwartet. Der Router empfängt Anforderungen an einen unidirektionalen Vorgang und speichert den Rückrufkanal des Clients, um die Antwort zu senden. In der Zwischenzeit leitet der Router Nachrichten unter Verwendung eines Duplexkanals weiter und stellt einen Rückrufkanal für den Empfang der Antwort vom Dienst bereit.
Abbildung 7 Duplexrouterarchitektur (Klicken Sie auf das Bild, um es zu vergrößern)
Der Dienst empfängt die Anforderung und sendet eine synchrone Antwort, die vom Rückrufkanal des Routers empfangen wird. Dieser Rückrufkanal verwendet wiederum den Rückrufkanal des Clients, um eine Antwort an den Client zurückzusenden. Von Anfang bis Ende verhält sich der Vorgang synchron. Der Router entkoppelt jedoch Aktivitäten und stützt sich auf Duplexkommunikation in den zugrunde liegenden Empfangs- und Sendekanälen, um Nachrichten zu korrelieren.
Die Routerimplementierung hierfür wird in Abbildung 8 gezeigt. Im Vergleich zu den Anforderung-Antwort-Routerimplementierungen gibt es einige wichtige Änderungen. Erstens unterstützt der Router Sitzungen und implementiert einen Duplexvertrag. Wenn der Router Nachrichten an Dienste weiterleitet, wird ein Duplexkanal unter Verwendung von „DuplexChannelFactory<T>“ erstellt. Dies bedeutet, es wird ein Rückrufobjekt bereitgestellt, das Antworten vom Dienst empfängt.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
  ConcurrencyMode = ConcurrencyMode.Multiple, 
  AddressFilterMode=AddressFilterMode.Any, 
  ValidateMustUnderstand=false)]

public class DuplexRouterService : IDuplexRouterService, IDisposable {
  object m_duplexSessionLock = new object();
  IDuplexRouterService m_duplexSession;

  public void ProcessMessage(Message requestMessage)  {
    lock (this.m_duplexSessionLock)  {
      if (this.m_duplexSession == null) {
        IDuplexRouterCallback callback = 
          OperationContext.Current.GetCallbackChannel
          <IDuplexRouterCallback>();

        DuplexChannelFactory<IDuplexRouterService> factory = 
          new DuplexChannelFactory<IDuplexRouterService>
          (new InstanceContext(null, 
          new DuplexRouterCallback(callback)), "serviceEndpoint");
        factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
        this.m_duplexSession = factory.CreateChannel();
      }
    }

    this.m_duplexSession.ProcessMessage(requestMessage);
  }
  public void Dispose() {
    if (this.m_duplexSession != null) {
      try {
        ICommunicationObject obj = this.m_duplexSession as 
          ICommunicationObject;
        if (obj.State == CommunicationState.Faulted)
          obj.Abort();
        else
          obj.Close();
      }
      catch {}
    }
  }
}

public class DuplexRouterCallback : IDuplexRouterCallback {

  private IDuplexRouterCallback m_clientCallback;

  public DuplexRouterCallback(IDuplexRouterCallback clientCallback) {
    m_clientCallback = clientCallback;
  }

  public void ProcessMessage(Message requestMessage) {
    this.m_clientCallback.ProcessMessage(requestMessage);
  }
}
Das Rückrufobjekt implementiert den Rückrufvertrag und empfängt Antworten vom Dienst. Dieses Rückrufobjekt muss den Rückrufkanal des Clients verwenden, um die Antwort an den Client zurückzugeben.
Die Routerdienstinstanz, die Rückrufkanalreferenz des Clients und der Routerrückrufkanal sind für die Dauer der Sitzung mit dem Client aktiv. Aus diesem Grund muss der Router Endpunkte verfügbar machen, die Sitzungen unterstützen, und der nachgeschaltete Dienst muss Sitzungen unterstützen, damit dies funktioniert.

Gemischte Transportsitzungen
In einigen Szenarios ist es möglicherweise wünschenswert, dass der Client über HTTP Nachrichten an den Router sendet, während der Router diese Nachrichten über TCP an Anwendungsdienste weiterleitet. Wenn Sicherheitsfeatures oder zuverlässige Sitzungen aktiviert sind, reicht sogar die Duplexrouterkonfiguration nicht aus, um das Szenario zu unterstützen.
Wie bereits erwähnt, wird die manuelle Adressierung nur bei Anforderung-Antwort-Kanälen unterstützt. Andernfalls stützt sich das Dienstmodell zur Korrelation von Nachrichten auf Adressierungsfeatures. Da TCP standardmäßig keine Unterstützung für Anforderung-Antwort bietet, ist die manuelle Adressierung nur bei unidirektionalen Verträgen möglich. Folglich muss der Sendekanal in Abbildung 7 auf der Basis eines unidirektionalen Vertrags wie „IDuplexRouterService“ erstellt werden. Der Rückrufkanal wird bereitgestellt, um die Antwort zu empfangen.
Der Rückrufkanal des Routers muss ebenfalls aktiv bleiben, bis die Antwort gesendet wurde. Auch der Rückrufkanal des Clients muss aktiv gehalten werden. Um dies zu unterstützen, muss der Client eine Sitzung mit dem Router, und der Router muss eine Sitzung mit dem Dienst unterhalten.
Unter der Voraussetzung, dass vom Router aufgerufene Anwendungsdienste sicher sind, muss wahrscheinlich die manuelle Adressierung verwendet werden, um Nachrichten unberührt vom Router weiterzuleiten. Wenn der Router Anwendungsdienste über TCP aufruft, erfordert dies eine Duplexrouterimplementierung (wie bereits besprochen), damit der ausgehende Aufruf ein unidirektionaler Kanal sein kann. Dies zwingt den Client, Nachrichten über eine Bindung zu senden, die Sitzungen erkennt, was bedeutet, dass sichere Sitzungen oder zuverlässige Sitzungen über HTTP aktiviert werden.
Wenn der Router ein Pass-Through-Router ist, kommt es darauf an, dem Anwendungsdienst die Verarbeitung von Sicherheitsheadern und Headern für zuverlässige Sitzungen zu ermöglichen. Wenn der Router sichere Sitzungen oder zuverlässige Sitzungen erfordert, damit seine Clientendpunkte Sitzungen über HTTP unterstützen, verarbeitet der Router diese Header, und die Sitzung wird nicht mit Anwendungsdiensten eingerichtet.
Folglich funktioniert das Mischen von Protokollen nur in begrenzten Szenarios, es sei denn, Sie nehmen Änderungen auf tieferer Ebene der Kanalschicht vor und setzen Standardverhaltensweisen außer Kraft. Wenn Sicherheit und zuverlässige Sitzungen deaktiviert sind, können Clients Nachrichten über HTTP an den Router senden, während der Router die Nachrichten über TCP an die Anwendungsdienste weiterleitet. Wenn Sicherheit oder zuverlässige Sitzungen aktiviert sind, muss der Client Nachrichten über TCP an den Router senden, damit die Sitzung eingerichtet werden kann, ohne dass zuverlässige Sitzungen oder sichere Sitzungen für den Routerkanal aktiviert werden.

Senden Sie Fragen und Kommentare in englischer Sprache an sstation@microsoft.com.


Michele Leroux Bustamante ist leitende Architektin bei IDesign Inc., Microsoft Regional Director für San Diego und Microsoft-MVP für verbundene Systeme. Ihr aktuelles Buch ist Learning WCF. Sie erreichen sie unter mlb@idesign.net, oder besuchen Sie idesign.net. Michele Leroux Bustamante führt einen Blog unter dasblonde.net.

Page view tracker