Freigeben über


Grundlagen

Discovery bietet neue Möglichkeiten für WCF

Juval Lowy

Beispielcode herunterladen.

Für alle mit Microsoft .NET Framework 3.5 möglichen WCF-Aufrufe (Windows Communication Foundation) gelten zwei Einschränkungen: Erstens muss der dem Dienst zugewiesene Port bzw. die Pipe verfügbar sein. Der Anwendungsentwickler oder Administrator muss buchstäblich raten oder den Port/die Pipe auf irgendeine Weise reservieren können. Zweitens muss der Client a priori die Adresse des Dienstendpunkts, die Portnummer und den Dienstcomputer oder den Pipenamen kennen.

Es wäre großartig, wenn der Dienst eine verfügbare Adresse nutzen könnte. Der Client müsste diese Adresse wiederum zur Laufzeit ermitteln. In der Tat gibt es eine auf Industriestandards basierende Lösung, die vorschreibt, wie diese Ermittlung vonstatten gehen soll. Diese Lösung, die einfach Discovery heißt, (und die zugehörigen Unterstützungsmechanismen) sind Thema dieses Artikels. Es werden auch einige nützliche Tools und Hilfsklassen vorgestellt. Der Quellcode hierfür ist verfügbar unter code.msdn.

Adressermittlung

Der Discovery-Mechanismus stützt sich auf das UDP-Protokoll (User Datagram Protocol) Im Gegensatz zu TCPP (Transmission Control Protocol) ist UDP ein verbindungsloses Protokoll, und zwischen dem Sender und dem Empfänger von Paketen ist keine direkte Verbindung erforderlich. Der Client verwendet UDP, um Discovery-Anforderungen an alle Endpunkte zu senden, die einen angegebenen Vertragstyp unterstützen. Diese Anforderungen werden von dedizierten Discovery-Endpunkten empfangen, die von den Diensten unterstützt werden. Die Implementierung des Discovery-Endpunkts antwortet dem Client mit der Adresse des Dienstendpunkts, der den angegebenen Vertrag unterstützt. Nachdem der Client die Dienste erkannt hat, ruft er sie wie gewöhnliche WCF-Aufrufe auf. Diese Sequenz ist in Abbildung 1 dargestellt.

Abbildung 1 Adressermittlung über UDP

image: Address Discovery over UDP

Ähnlich wie der MEX-Endpunkt (Metadata Exchange) bietet WCF einen Standard-Discovery-Endpunkt mit dem Typ UdpDiscoveryEndpoint:

          public class DiscoveryEndpoint : ServiceEndpoint

          {...}

          public class UdpDiscoveryEndpoint : DiscoveryEndpoint

          {...}
        

Der Dienst kann diesen Endpunkt vom Host implementieren lassen, indem er ServiceDiscoveryBehavior den Auflistungen der von ihm unterstützten Verhalten hinzufügt. Dies kann mit folgendem Programmcode geschehen:.

          ServiceHost host = new ServiceHost(...);
          host.AddServiceEndpoint(new UdpDiscoveryEndpoint());
          ServiceDiscoveryBehavior discovery = new ServiceDiscoveryBehavior();
          host.Description.Behaviors.Add(discovery);
          host.Open();
        

Abbildung 2 zeigt, wie der Discovery-Endpunkt und das Discovery-Verhalten mit der Dienstkonfigurationsdatei hinzugefügt wird.

Abbildung 2 Hinzufügen eines Discovery-Endpunkts in der Konfigurationsdatei

          <services>
          <service name = "MyService">
          <endpoint
          kind = "udpDiscoveryEndpoint"
          />
          ...
          </service>
          </services>
          <behaviors>
          <serviceBehaviors>
          <behavior>
          <serviceDiscovery/>
          </behavior>
          </serviceBehaviors>
          </behaviors>
        

Dynamische Adressen

Die Ermittlung ist unabhängig davon, wie der Diensthost seine Endpunkte definiert. Wie sieht es aber aus, wenn vom Client erwartet wird, dass er mithilfe des Discovery-Mechanismus die Dienstadresse ermittelt? In diesem Fall steht es dem Dienst frei, seine Endpunktadressen dynamisch, basierend auf der Verfügbarkeit eines Ports oder einer Pipe zu konfigurieren. 

Um die Verwendung automatischer Adressen zu automatisieren, habe ich die statische Hilfsklasse DiscoveryHelper mit den beiden Eigenschaften AvailableIpcBaseAddress und AvailableTcpBaseAddress geschrieben:

          public static class DiscoveryHelper
          {
          public static Uri AvailableIpcBaseAddress
          {get;}
          public static Uri AvailableTcpBaseAddress
          {get;}
          }
        

Die AvailableIpcBaseAddress-Eigenschaft lässt sich ganz einfach implementieren, weil jede eindeutig benannte Pipe akzeptiert wird. Die Eigenschaft verwendet ein neuer GUID (Globally Unique Identifier) zur Benennung der Pipe.  Zur Implementierung von AvailableTcpBaseAddress wird ein verfügbarer TCP-Port gesucht, indem Port 0 geöffnet wird.

Abbildung 3 zeigt, wie AvailableTcpBaseAddress verwendet wird.

Abbildung 3 Verwenden dynamischer Adressen

          Uri baseAddress = DiscoveryHelper.AvailableTcpBaseAddress;
          ServiceHost host = new ServiceHost(typeof(MyService),baseAddress);
          host.AddDefaultEndpoints();
          host.Open();
          <service name = "MyService">
          <endpoint
          kind = "udpDiscoveryEndpoint"
          />
          </service>
          <serviceBehaviors>
          <behavior>
          <serviceDiscovery/>
          </behavior>
          </serviceBehaviors>
        

Wenn Sie nur die dynamische Basisadresse des Diensts ermitteln möchten, ist der Code in Abbildung 3 keineswegs perfekt, weil Sie entweder in der Konfigurationsdatei oder im Programmcode noch den Discovery-Mechanismus hinzufügen müssen. Sie können diese Schritte mit meiner Hosterweiterung EnableDiscovery optimieren, die wie folgt definiert ist:

          public static class DiscoveryHelper
          {
          public static void EnableDiscovery(this ServiceHost host,bool enableMEX = true);
          }
        

Beim Einsatz von EnableDiscovery ist weder zusätzlicher Programmcode noch eine Konfigurationsdatei erforderlich:

          Uri baseAddress = DiscoveryHelper.AvailableTcpBaseAddress;
          ServiceHost host = new ServiceHost(typeof(MyService),baseAddress);
          host.EnableDiscovery();
          host.Open();
        

Wenn der Host die Endpunkte für den Dienst nicht bereits definiert, fügt EnableDiscovery die Standardendpunkte hinzu. EnableDiscovery fügt standardmäßig dem Dienst auch den MEX-Endpunkt an seinen Basisadressen hinzu.

Clientseitige Schritte

Der Client verwendet die DiscoveryClient-Klasse, um alle Endpunktadressen aller Dienste zu ermitteln, die einen angegebenen Vertrag unterstützen.

          public sealed class DiscoveryClient : ICommunicationObject
          {
          public DiscoveryClient();
          public DiscoveryClient(string endpointName);
          public DiscoveryClient(DiscoveryEndpoint discoveryEndpoint);
          public FindResponse Find(FindCriteria criteria);
          //More members
          }
        

Von der Programmlogik her gesehen, ist DiscoveryClient ein Proxy für den Discovery-Endpunkt. Wie bei allen Proxys muss der Client den Konstruktor des Proxys mit den Informationen über den Zielpunkt bereitstellen. Der Client kann zu diesem Zweck eine Konfigurationsdatei zur Angabe des Endpunkts verwenden oder den Standard-UDP-Discovery-Endpunkt im Programmcode angeben, da keine weiteren Details (z. B. eine Adresse oder Bindung) erforderlich sind. Der Client ruft dann die Find-Methode auf, und übergibt ihr über eine FindCriteria-Instanz den Vertragstyp, der ermittelt werden soll.

          public class FindCriteria

          {

          public FindCriteria(Type contractType);
          //More members

          }
        

Die Find-Methode gibt eine Instanz von FindResponse zurück, die eine Auflistung aller ermittelten Endpunkte enthält:

          public class FindResponse

          {
          public Collection<EndpointDiscoveryMetadata> Endpoints
          {get;}
          //More members
          }
        

Jeder Endpunkt wird durch die EndpointDiscoveryMetadata-Klasse repräsentiert:

          public class EndpointDiscoveryMetadata
          {
          public EndpointAddress Address
          {get;set;}
          //More members
          }
        

Die Haupteigenschaft der EndpointDiscoveryMetadata-Klasse heißt Address und enthält schließlich die ermittelte Endpunktadresse. Abbildung 4 zeigt, wie der Client durch die Kombination dieser Typen die Endpunktadresse ermitteln und den Dienst aufrufen kann.

Abbildung 4 Ermitteln und Aufrufen eines Endpunkts

          DiscoveryClient discoveryClient =
          new DiscoveryClient(new UdpDiscoveryEndpoint());
          FindCriteria criteria = new FindCriteria(typeof(IMyContract));
          FindResponse discovered = discoveryClient.Find(criteria);
          discoveryClient.Close();
          //Just grab the first found
          EndpointAddress address = discovered.Endpoints[0].Address;
          Binding binding = new NetTcpBinding();
          IMyContract proxy =
          ChannelFactory<IMyContract>.CreateChannel(binding,address);
          proxy.MyMethod();
          (proxy as ICommunicationObject).Close();
        

Abbildung 4 enthält verschiedene bemerkenswerte Probleme.

Der Client kann zwar mehrere Endpunkte ermitteln, die den gewünschten Vertrag unterstützten, aber es ist keine Logik vorhanden, mit der der aufzurufende Endpunkt bestimmt werden kann. Es wird einfach der erste Endpunkt in der zurückgegebenen Auflistung aufgerufen.

Der Discovery-Mechanismus ist nur auf Adressen ausgerichtet. Es gibt keine Informationen dazu, welche Bindung zum Aufrufen des Diensts verwendet werden soll. In Abbildung 4 ist die Nutzung der TCP-Bindung einfach hartcodiert. Der Client muss diese hier dargestellten Schritte jedes Mal, wenn er die Dienstadresse ermitteln muss, wiederholen.

Die Ermittlung ist zeitaufwändig. Standardmäßig wartet die Find-Methode 20 Sekunden lang auf eine Antwort der Dienste auf die UDP-Discovery-Anforderung. Wegen dieser Verzögerung ist der Discovery-Mechanismus für die meisten Anwendungen ungeeignet, insbesondere, wenn die Anwendung eine Menge zeitkritischer Aufrufe ausführt. Das Zeitlimit könnte zwar verkürzt werden, aber wenn Sie dies tun, besteht die Gefahr, dass keine oder nicht alle Dienste ermittelt werden. DiscoveryClient stellt einen asynchronen Discovery-Mechanismus zur Verfügung. Dieser ist jedoch für Clients nutzlos, die den Dienst aufrufen müssen, bevor sie mit der Ausführung fortfahren können. 

In diesem Artikel werden verschiedene Ansätze zur Behandlung dieser Probleme vorgestellt.

Geltungsbereiche

Die Verwendung des Discovery-Mechanismus impliziert eine relativ lockere Beziehung zwischen Client und dem ermittelten Dienst bzw. den ermittelten Diensten. Dadurch ergibt sich eine andere Gruppe von Aufgaben: Woher weiß der Client, dass er den richten Endpunkt ermittelt hat? Welcher Endpunkt soll aufgerufen werden, wenn mehrere kompatible Endpunkte ermittelt werden?

Es muss eindeutig irgendeinen Mechanismus geben, mit dem der Client die Ermittlungsergebnisse filtern kann. Genau hierfür sind Geltungsbereiche gedacht. Ein Geltungsbereich ist ein einfach eine gültige URL, die dem Endpunkt zugeordnet ist. Der Dienst kann mit jedem Endpunkt einen Geltungsbereich oder sogar mehrere Geltungsbereiche verknüpfen. Die Geltungsbereiche werden zusammen mit den Adressen in die Antwort auf die Discovery-Anforderung eingefügt. Der Client wiederum kann die ermittelten Adressen anhand der gefundenen Geltungsbereiche filtern, oder noch besser, gleich versuchen, nur die relevanten Geltungsbereiche zu finden.

Geltungsbereiche sind immens nützlich bei der Anpassung des Discovery-Mechanismus und beim Hinzufügen intelligenten Verhaltens zu einer Anwendung, insbesondere beim Entwickeln eines Frameworks oder von Verwaltungstools. Traditionell werden Geltungsbereiche verwendet, um den Client in die Lage zu versetzen, polymorphe Dienste verschiedener Anwendungen voneinander zu unterscheiden. Dies kommt allerdings eher selten vor. Ich finde Geltungsbereiche praktisch, wenn zwischen verschiedenen Endpunkttypen innerhalb einer Anwendung unterschieden werden muss.

Nehmen wir beispielsweise an, für einen gegebenen Vertrag sind mehrere Implementierungen vorhanden. In der Produktion wurde der Betriebsmodus und für Tests oder die Diagnose wurde der Simulationsmodus verwendet. Mithilfe von Geltungsbereichen kann der Client den erforderlichen Implementierungstyp auswählen, und verschiedene Clients geraten nie miteinander in Konflikt darüber, dass sie die Dienste anderer Clients nutzen. Ein Client kann auch je nach Aufrufkontext verschiedene Endpunkte auswählen. Es könnte Endpunkte für die Profilerstellung, das Debuggen, das Diagnostizieren, das Testen, die Instrumentation usw. geben. 

Der Host weist mithilfe der EndpointDiscoveryBehavior-Klasse den verschiedenen Endpunkten unterschiedliche Geltungsbereiche zu. Um beispielsweise allen Endpunkten einen Geltungsbereich zuzuweisen, verwenden Sie das Standardendpunktverhalten.

          <endpointBehaviors>
          <behavior>
          <endpointDiscovery>
          <scopes>
          <add scope = "net.tcp://MyApplication"/>
          </scopes>
          </endpointDiscovery>
          </behavior>
          </endpointBehaviors>
        

Die Geltungsbereiche werden diskret, auf dem Diensttyp basierend angewendet, indem wie in Abbildung 5 gezeigt, Verhalten explizit den einzelnen Endpunkten zugewiesen werden.

Abbildung 5 Explizite Verhaltenszuweisung

          <service name = "MySimulator">
          <endpoint behaviorConfiguration = "SimulationScope"
          ...
          />
          ...
          </service>
          ...
          <behavior name = "SimulationScope">
          <endpointDiscovery>
          <scopes>
          <add scope = "net.tcp://Simulation"/>
          </scopes>
          </endpointDiscovery>
          </behavior>
        

Ein Discovery-Verhalten kann mehrere Geltungsbereiche auflisten:

          <endpointDiscovery>
          <scopes>
          <add scope = "net.tcp://MyScope1"/>
          <add scope = "net.tcp://MyScope2"/>
          </scopes>
          </endpointDiscovery>
        

Wenn einem Endpunkt mehrere Geltungsbereiche zugeordnet sind und der Client versucht, den Endpunkt durch Abgleich der Geltungsbereiche zu ermitteln, muss der Client mindestens einem Geltungsbereich entsprechen, aber nicht allen Geltungsbereichen.

Der Client kann Geltungsbereiche auf zweierlei Weise verwenden. Erstens kann er den Geltungsbereich den durch FindCriteria definierten Suchkriterien hinzufügen:

          public class FindCriteria
          {
          public Collection<Uri> Scopes
          {get;}
          //More members
          }
        

Daraufhin gibt die Find-Methode nur kompatible Endpunkte zurück, die auch diesen Geltungsbereich angeben. Wenn der Client mehrere Geltungsbereiche hinzufügt, gibt die Find-Methode nur diejenigen Endpunkte zurück, die alle aufgeführten Geltungsbereiche unterstützen. Beachten Sie, dass der Endpunkt möglicherweise weitere Geltungsbereiche unterstützt, die die Find-Methode nicht kennt.

Die zweite Möglichkeit, Geltungsbereiche zu verwenden, besteht darin, die in FindResponse zurückgegebenen Geltungsbereiche zu überprüfen.

          public class EndpointDiscoveryMetadata
          {
          public Collection<Uri> Scopes
          {get;}
          //More members
          }
        

Das sind alle vom Endpunkt unterstützten Geltungsbereiche. Sie sind hilfreich, wenn weiter gefiltert werden soll. 

Discovery-Kardinalität

Sobald sich ein Client auf Discovery verlässt, muss er sich mit der von mir so genannten Discovery-Kardinalität befassen, also damit, wie viele Endpunkte ermittelt werden und welcher dieser Endpunkte, sofern gegeben, aufgerufen werden soll. Es lassen sich verschiedene Fälle von Kardinalität unterscheiden.

  • Es wird kein Endpunkt erkannt. In diesem Fall muss der Client damit umgehen, dass kein Dienst vorhanden ist. Dies entspricht der Situation eines WCF-Clients, dessen Dienst nicht verfügbar ist.
  • Es wird genau ein kompatibler Endpunkt gefunden. Dies ist der bei weitem häufigste Fall. Der Client ruft daraufhin einfach den Dienst auf.
  • Es werden mehrere Endpunkte ermittelt. Hier hat der Client theoretisch zwei Optionen. Die erste Option ist, alle Endpunkte aufzurufen. Wie weiter unten besprochen wird, ist dies bei einem Verleger der Fall, der ein Ereignis bei Abonnenten auslöst, und ein gültiges Szenario. Die zweite Option besteht darin, einige (möglicherweise genau einen), jedoch nicht alle ermittelten Endpunkte aufzurufen. Ich halte dieses Szenario für hypothetisch. Jeder Versuch, den Client mit Logik auszustatten, mit der er herausfinden kann, welcher Endpunkt aufgerufen werden soll, erzeugt im System zu viel Koppelung. Damit wird das eigentliche Konzept der Laufzeit-Discovery negiert, nämlich, dass es genügt, irgendeinen Endpunkt zu ermitteln. Wenn es möglich ist, unerwünschte Endpunkte zu ermitteln, dann ist der Discovery-Mechanismus eine schlechte Wahl, und dem Client sollten stattdessen statische Adressen übergeben werden.

Wenn vom Client erwartet wird, dass er genau einen Endpunkt (Kardinalität 1) ermittelt, dann sollte der Client Find anweisen, sofort nach der Ermittlung dieses Endpunkt zurückzukehren. Auf diese Weise lässt sich die Discovery-Latenz drastisch reduzieren, sodass die Discovery-Methode für die Mehrzahl der Fälle ein probates Mittel darstellt.

Der Client kann die Kardinalität mithilfe der MaxResults-Eigenschaft von FindCriteria konfigurieren:

          public class FindCriteria
          {
          public int MaxResults
          {get;set;}
          //More members
          }
          FindCriteria criteria = new FindCriteria(typeof(IMyContract));
          criteria.MaxResults = 1;
        

Der Fall einer Kardinalität von 1 kann mithilfe der Hilfsmethode Discovery​Helper.DiscoverAddress<T> optimiert werden:

          public static class DiscoveryHelper
          {
          public static EndpointAddress DiscoverAddress<T>(Uri scope = null);
          //More members
          }
        

Durch den Einsatz von DiscoverAddress<T>, wird der in Abbildung 4 dargestellte Code reduziert auf Folgendes:

          EndpointAddress address = DiscoveryHelper.DiscoverAddress<IMyContract>();
          Binding binding = new NetTcpBinding();
          IMyContract proxy = ChannelFactory<IMyContract>.CreateChannel(binding,address);
          proxy.MyMethod();
          (proxy as ICommunicationObject).Close();
        

Optimieren des Discovery-Vorgangs

Bislang musste die zu verwendende Bindung im Client hartcodiert werden. Wenn der Dienst allerdings MEX-Endpunkte unterstützt, kann der Client eine MEX-Endpunktadresse ermitteln, anschließend die Metadaten abrufen und verarbeiten, um die zu verwendende Bindung zusammen mit der Endpunktadresse zu erhalten. Um die Ermittlung von MEX-Endpunkten zu erleichtern, stellt die FindCriteria-Klasse die statische CreateMetadataExchangeEndpointCriteria-Methode zur Verfügung.

          public class FindCriteria
          {
          public static FindCriteria CreateMetadataExchangeEndpointCriteria();
          //More members
          }
        

Zur Optimierung dieser Sequenz wird die DiscoveryFactory.CreateChannel<T>-Methode verwendet:

          public static class DiscoveryFactory
          {
          public static T CreateChannel<T>(Uri scope = null);
          //More members
          }
        

Durch den Einsatz von CreateChannel<T> wird der in Abbildung 4 dargestellte Code reduziert auf Folgendes:

          IMyContract proxy = DiscoveryFactory.CreateChannel<IMyContract>();
          proxy.MyMethod();
          (proxy as ICommunicationObject).Close();
        

CreateChannel<T> unterstellt bezüglich des MEX-Endpunkts eine Kardinalität von 1 (das heißt, im lokalen Netzwerk kann nur ein MEX-Endpunkt ermittelt werden) und dass die Metadaten genau einen Endpunkt enthalten, dessen Vertrag mit dem Parameter T angegeben wird.

Beachten Sie, dass der MEX-Endpunkt in CreateChannel<T> sowohl für die Endpunktbindung als auch für die Adresse verwendet wird. Vom Dienst wird erwartet, dass er sowohl einen MEX-Endpunkt als auch einen Discovery-Endpunkt unterstützt (obwohl der Client den Discovery-Endpunkt nie verwendet, um den tatsächlichen Endpunkt zu finden).

Für den Fall, dass mehrere Dienste den gewünschten Dienstvertrag unterstützen oder mehrere MEX-Endpunkte vorhanden sind, stellt DiscoveryFactory die CreateChannels<T>-Methode bereit.

          public static class DiscoveryHelper
          {
          public static T[] CreateChannels<T>(bool inferBinding = true);
          //More members
          }
        

Standardmäßig schließt CreateChannels<T> aus dem Schema des Dienstendpunkts auf die zu verwendende Bindung. Wenn inferBinding zu false ausgewertet wird, dann wird die Bindung anhand der MEX-Endpunkte ermittelt.

CreateChannels<T> unterstellt weder bei den kompatiblen Endpunkten noch den MEX-Endpunkten eine Kardinalität von 1 und gibt ein Array mit allen kompatiblen Endpunkten zurück.

Ankündigungen

Der bislang vorgestellte Discovery-Mechanismus ist von den Diensten her gesehen passiv. Der Client fordert den Discovery-Endpunkt an, und der Dienst antwortet. Als Alternative zu dieser passiven Adressermittlung bietet WCF ein aktives Modell an, in dem der Dienst seinen Status allen Clients bekannt gibt und seine Adresse bereitstellt. Der Diensthost sendet eine Ankündigung mit dem Inhalt "hello", wenn der Host geöffnet wird, und eine Ankündigung mit dem Inhalt "bye", wenn der Host normal heruntergefahren wird. Wird der Host mit einer Fehlerbedingung beendet, dann wird keine "bye"-Ankündigung gesendet. Diese Ankündigungen werden an einem speziellen Ankündigungsendpunkt empfangen, der vom Client gehostet wird (siehe Abbildung 6).

Abbildung 6 Die Ankündigungsarchitektur

image: The Announcement Architecture

Ankündigungen sind Mechanismen auf der Ebene einzelner Endpunkte, nicht auf der Hostebene. Der Host kann entscheiden, welcher Endpunkt angekündigt wird. Jede Ankündigung enthält die Adresse, die Geltungsbereiche und den Vertrag des Endpunkts.

Beachten Sie, dass Ankündigungen von der Adressermittlung unabhängig sind. Der Host unterstützt möglicherweise gar keinen Discovery-Endpunkt, und es ist kein Discovery-Verhalten erforderlich. Andererseits kann der Host sowohl den Discovery-Endpunkt unterstützen als auch seine Endpunkte ankündigen, wie in -Abbildung 6 gezeigt.

Der Host kann seine Endpunkte automatisch ankündigen. Dazu müssen lediglich die Informationen über den Ankündigungsendpunkt des Clients für das Discovery-Verhalten bereitgestellt werden. Bei Verwendung einer Konfigurationsdatei sieht das folgendermaßen aus:

          <behavior>
          <serviceDiscovery>
          <announcementEndpoints>
          <endpoint
          kind = "udpAnnouncementEndpoint"
          />
          </announcementEndpoints>
          </serviceDiscovery>
          </behavior>
        

Die EnableDiscovery-Erweiterungsmethode fügt auch den Ankündigungsendpunkt dem Discovery-Verhalten hinzu.

Wie in Abbildung 7 dargestellt, stellt WCF mit der AnnouncementService-Klasse eine vorgefertigte Implementierung eines Ankündigungsendpunkts für Clients bereit.

Abbildung 7 WCF-Implementierung eines Ankündigungsendpunkts

          public class AnnouncementEventArgs : EventArgs
          {
          public EndpointDiscoveryMetadata EndpointDiscoveryMetadata
          {get;}
          //More members
          }
          [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
          ConcurrencyMode = ConcurrencyMode.Multiple)]
          public class AnnouncementService : ...
          {
          public event EventHandler<AnnouncementEventArgs> OfflineAnnouncementReceived;
          public event EventHandler<AnnouncementEventArgs> OnlineAnnouncementReceived;
          //More members
          }
        

Die AnnouncementService-Klasse ist ein für den gleichzeitigen Zugriff konfiguriertes Singleton. AnnouncementService stellt zwei Ereignisdelegaten zur Verfügung, die der Client abonnieren kann, um die Ankündigungen zu empfangen. Der Client sollte die AnnouncementService-Klasse hosten, indem er den Konstruktor von ServiceHost verwendet, der eine Singleton-Instanz akzeptiert. Das ist erforderlich, damit der Client mit der Instanz zusammenarbeiten und die Ereignisse abonnieren kann. Außerdem muss der Client den UDP-Ankündigungsendpunkt dem Host hinzufügen.

          AnnouncementService announcementService = new AnnouncementService();
          announcementService.OnlineAnnouncementReceived  += OnHello;
          announcementService.OfflineAnnouncementReceived += OnBye;
          ServiceHost host = new ServiceHost(announcementService);
          host.AddServiceEndpoint(new UdpAnnouncementEndpoint());
          host.Open();
          void OnHello(object sender,AnnouncementEventArgs args)
          {...}
          void OnBye(object sender,AnnouncementEventArgs args)
          {...}
        

Es gibt ein wichtiges Detail in Bezug auf den Empfang von Ankündigungen. Der Client würde alle Benachrichtigungen von allen Diensten im Intranet empfangen, unabhängig vom Vertragstyp bzw. von Anwendungen und Geltungsbereichen. Der Client muss die relevanten Ankündigungen herausfiltern.

Ankündigungen optimieren.

Mit der AnnouncementSink<T>-Klasse können die Schritte, die der Client zur Nutzung von Ankündigungen ausführen muss, stark vereinfacht und verbessert werden. Es folgt die Definition diese Klasse:

          public class AnnouncementSink<T> : AddressesContainer<T> where T: class
          {
          public event Action<T> OnHelloEvent;
          public event Action<T> OnByeEvent;
          }
        

Die Klasse AnnouncementSink<T> automatisiert das Hosten der Ankündigungsendpunkte, indem sie die Schritte aus Abbildung 7 kapselt. AnnouncementSink<T> hostet zwar intern eine Instanz von AnnouncementService, gleicht jedoch deren Mängel aus. Erstens stellt AnnouncementSink<T> zwei Ereignisdelegaten für Benachrichtigungen bereit. Im Gegensatz zur AnnouncementService-Klasse löst AnnouncementSink<T> diese Delegaten gleichzeitig aus. Zudem deaktiviert AnnouncementSink<T> die Synchronisierungskontextaffinität von AnnouncementService, sodass sie die Ankündigungen von jedem eingehenden Thread akzeptieren kann, wodurch echte Parallelität ermöglicht wird.

AnnouncementSink<T> filtert die Vertragstypen und löst ihre Ereignisse nur dann aus, wenn sie Ankündigungen von kompatiblen Endpunkten empfängt. Der Client muss lediglich eine AnnouncementSink<T>-Instanz öffnen und schließen, um anzugeben, wann mit dem Empfang von Nachrichten begonnen und wann er beendet werden soll.

AnnouncementSink<T> leitet die Allzweckadresscontainerklasse namens AddressesContainer<T> ab.

AddressesContainer<T> ist eine reichhaltige Sammlung von Hilfsmitteln für die Adressverwaltung, die Sie immer dann einsetzen können, wenn Sie mehrere Adressen bearbeiten müssen. AddressesContainer<T> unterstützt einige Iteratoren, Indexer, Konvertierungsmethoden und Abfragen.

Abbildung 8 veranschaulicht die Verwendung von AnnouncementSink<T>.

Abbildung 8 Verwendung von AnnouncementSink<T>

          class MyClient : IDisposable
          {
          AnnouncementSink<IMyContract> m_AnnouncementSink;
          public MyClient()
          {
          m_AnnouncementSink = new AnnouncementSink<IMyContract>();
          m_AnnouncementSink.OnHelloEvent += OnHello;
          m_AnnouncementSink.Open();
          }
          void Dispose()
          {
          m_AnnouncementSink.Close();
          }
          void OnHello(string address)
          {
          EndpointAddress endpointAddress = new EndpointAddress(address);
          IMyContract proxy = ChannelFactory<IMyContract>.CreateChannel(
          new NetTcpBinding(),endpointAddress);
          proxy.MyMethod();
          (proxy as ICommunicationObject).Close();
          }
          }
        

Der MEX Explorer

In meinem Buch mit dem Titel Programming WCF Services Second Edition (O’Reilly, 2008) stellte ich ein Tool vor, das ich MEX Explorer nannte (siehe Abbildung 9). Sie können dem MEX Explorer eine MEX-Adresse übergeben und von ihm deren Dienstpunkte ausgeben lassen (ihre Adresse, die Bindungseigenschaften und den Vertrag). Die Einführung des Discovery-Mechanismus ermöglichte es mir, den MEX Explorer umzugestalten.

Abbildung 9 Der MEX Explorer

image: The MEX Explorer

Durch Klicken auf die Schaltfläche "Discover" wird eine Discovery-Anforderung zur Ermittlung sämtlicher MEX-Endpunkte, die keine Beschränkung hinsichtlich der Kardinalität aufweist, generiert. Das Tool stellt dann alle erkannten Endpunkte in der Struktur grafisch dar. Zudem nutzt der MEX Explorer die Ankündigung von MEX-Endpunkten. Der MEX Explorer reagiert auf die Ankündigungen und aktualisiert seine Anzeige dadurch selbst, indem er neue Endpunkte hinzufügt bzw. diejenigen Endpunkte aus der Struktur entfernt, die nicht mehr ausgeführt werden.

Durch Discovery gesteuertes Veröffentlichen-Abonnieren-Musters

In dem im Oktober 2006 erschienenen Artikel mit dem Titel What You Need To Know About One-Way Calls, Callbacks and Events stellte ich mein Framework zur Unterstützung eines Veröffentlichen-Abonnieren-Musters in WCF vor. Sie können den Discovery-Mechanismus und Ankündigungen verwenden, um ein Veröffentlichen-Abonnieren-System auf eine wiederum andere Art zu implementieren.

Im Gegensatz zu den in diesem Artikel beschriebenen Techniken, ist die auf dem Discovery-Mechanismus basierende Lösung der einzige Fall, in dem die Abonnenten oder der Administrator keine expliziten Schritte ausführen müssen. Beim Einsatz von Discovery ist es nicht notwendig, im Code oder in der Konfigurationsdatei ein explizites Abonnement zu definieren. Dadurch wird die Bereitstellung des Systems wiederum stark vereinfacht und die Darstellung von Verleger und Abonnent äußerst flexibel. Sie können mühelos weitere Abonnenten oder Verleger hinzufügen oder entfernen, ohne zusätzliche Verwaltungsschritte ausführen oder programmieren zu müssen.

Wenn der Discovery-Mechanismus für ein Veröffentlichen-Abonnieren-System genutzt wird, können die Abonnenten einen Discovery-Endpunkt bereitstellen, damit sie vom Veröffentlichen-Abonnieren-Dienst ermittelt werden können, oder sie können ihre Ereigenisbehandlungsendpunkte ankündigen oder sogar beides tun.

Der Verleger sollte die Abonnenten nicht direkt ermitteln, weil sich dadurch bei jedem ausgelösten Ereignis (mit einer Kardinalität, die der Anzahl sämtlicher Endpunkte entspricht) eine Discovery-Latenz erzeugt werden könnte. Stattdessen sollten die Verleger den Veröffentlichen-Abonnieren-Dienst ermitteln, was eine einmalige, mit keinen nennenswerten Kosten verbundene Angelegenheit ist. Der Veröffentlichen-Abonnieren-Dienst sollte ein Singleton sein (das durch seine Kardinalität von 1 eine schnelle Ermittlung ermöglicht). Da der Veröffentlichen-Abonnieren-Dienst denselben Ereignisendpunkt wie die Abonnenten verfügbar macht, stellt er sich für die Verleger wie ein Meta-Abonnent dar. Das heißt, zum Auslösen von Ereignissen im Veröffentlichen-Abonnieren-Dienst ist der gleiche Code erforderlich wie zum Auslösen von Ereignissen bei einem Abonnenten.

Der Ereignisendpunkt des Veröffentlichen-Abonnieren-Diensts muss einen bestimmten Geltungsbereich haben. Dieser Geltungsbereich ermöglicht es den Verlegern, den Veröffentlichen-Abonnieren-Dienst statt der Abonnenten zu finden. Der Veröffentlichen-Abonnieren-Dienst unterstützt nicht nur das Ermitteln von Ereignispunkten mit definiertem Gültigkeitsbereich, sondern er stellt auch einen Ankündigungsendpunkt bereit.

Der Veröffentlichen-Abonnieren-Dienst verwaltet eine Liste aller Abonnenten. Der Veröffentlichen-Abonnieren-Dienst hält diese Liste auf dem Laufenden, indem er ständig unter Verwendung einer laufenden Hintergrundaktivität versucht, die Abonnenten zu ermitteln. Beachten Sie auch hier, dass durch die Zuordnung des Ereignisendpunkts zu einem Geltungsbereich verhindert wird, dass sich der Veröffentlichen-Abonnieren-Dienst beim Ermitteln sämtlicher Ereignisendpunkte selbst ermittelt. Der Veröffentlichen-Abonnieren-Dienst kann auch einen Ankündigungsendpunkt zum Überwachen der Abonnenten bereitstellen. In Abbildung 10 ist diese Architektur dargestellt.

Abbildung 10 Durch Discovery gesteuertes Veröffentlichen-Abonnieren-System

image: Discovery-Driven Publish-Subscribe System

Der Veröffentlichen-Abonnieren-Dienst

Um die Bereitstellung eines eigenen Veröffentlichen-Abonnieren-Diensts zu erleichtern, wurde die DiscoveryPublishService<T>-Klasse wie folgt definiert

          [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
          public class DiscoveryPublishService<T> : IDisposable where T: class
          {
          public static readonly Uri Scope;
          protected void FireEvent(params object[] args);
          //More members
          }
        

Sie müssen lediglich einen eigenen Veröffentlichen-Abonnieren-Dienst von DiscoveryPublishService<T> ableiten und den Ereignisvertrag als Typparameter angeben. Dann implementieren Sie die Vorgänge des Ereignisvertrags durch einen Aufruf der FireEvent-Methode.

Betrachten Sie beispielsweise den folgenden Ereignisvertrag:

          [ServiceContract]
          interface IMyEvents
          {
          [OperationContract(IsOneWay = true)]
          void OnEvent1();
          [OperationContract(IsOneWay = true)]
          void OnEvent2(int number);
          }
        

Abbildung 11 zeigt, wie ein Veröffentlichen-Abonnieren-Dienst mithilfe von DiscoveryPublishService<T> implementiert wird.

Abbildung 11 Implementieren eines Veröffentlichen-Abonnieren-Diensts

          class MyPublishService : DiscoveryPublishService<IMyEvents>,IMyEvents
          {
          public void OnEvent1()
          {
          FireEvent();
          }
          public void OnEvent2(int number)
          {
          FireEvent(number);
          }
          }
        

Intern verwendet DiscoveryPublishService<T> eine andere von AddressContainer<T> abgeleitete Klasse namens DiscoveredServices<T>, die wie folgt definiert ist:

          public class DiscoveredServices<T> : AddressesContainer<T> where T: class
          {
          public DiscoveredServices();
          public void Abort();
          }
        

Die DiscoveredServices<T>-Klasse ist darauf ausgelegt, eine möglichste aktuelle Liste aller ermittelten Dienste zu verwalten und die erkannten Adressen in ihrer Basisklasse zu speichern. DiscoveredServices<T> startet in einem Hintergrundthread einen fortlaufenden Erkennungsvorgang, und dies in den Fällen hilfreich, in denen ein aktuelles Repository der ermittelten Adressen verfügbar sein soll.

Die FireEvent-Methode extrahiert den Namen des Vorgangs aus dem Nachrichtenheader. Dann fragt sie die Abonnentenliste nach allen Abonnenten ab, die den Geltungsbereich des Veröffentlichen-Abonnieren-Diensts nicht unterstützen (um eine Selbstermittlung zu vermeiden). Die FireEvent-Methode fügt die Listen in eine Union eindeutiger Einträge ein (dies ist erforderlich, um Abonnenten handhaben zu können, die sich sowohl selbst ankündigen als auch ermittelbar sind). Bei jedem Abonnenten schließt die FireEvent-Methode aus dem Adressschema auf die Bindung und erstellt ein Proxy, das beim Abonnenten ausgelöst werden soll. Die Veröffentlichung der Ereignisse erfolgt gleichzeitig mit Threads aus dem Threadpool.

Verwenden Sie die statische Hilfsmethode CreateHost<S>, um den Veröffentlichen-Abonnieren-Dienst zu hosten:

          public class DiscoveryPublishService<T> : IDisposable where T: class
          {
          public static ServiceHost<S> CreateHost<S>()
          where S : DiscoveryPublishService<T>,T;
          //More members
          }
        

Der Typparameter S enthält die Unterklasse von DiscoveryPublishService<T>, und T enthält den Ereignisvertrag. CreateHost<S> gibt eine Instanz des zu öffnenden Diensthosts zurück.

          ServiceHost host = DiscoveryPublishService<IMyEvents>.
          CreateHost<MyPublishService>();
          host.Open();
        

Außerdem ruft CreateHost<S> eine verfügbare TCP-Basisadresse ab und fügt den Ereignisendpunkt hinzu, sodass keine Konfigurationsdatei erforderlich ist.

Der Verleger

Der Verleger benötigt einen Proxy für den Ereignisdienst. Verwenden Sie hierzu DiscoveryPublishService<T>.CreateChannel:

          public class DiscoveryPublishService<T> : IDisposable where T : class
          {
          public static T CreateChannel();
          //More members
          }
        

Die DiscoveryPublishService<T>.CreateChannel-Methode ermittelt den Veröffentlichen-Abonnieren-Dienst und erstellt einen Proxy dafür. Der Dienst wird schnell ermittelt, da er seine Kardinalität 1 beträgt. Der Code für den Verleger ist einfach:

          IMyEvents proxy = DiscoveryPublishService<IMyEvents>.CreateChannel();
          proxy.OnEvent1();
          (proxy as ICommunicationObject).Close();
        

Die Implementierung eines Abonnenten ist ebenso einfach: Er muss einfach den Ereignisvertrag des Diensts unterstützen, und zudem muss ein Discovery-Mechanismus oder Ankündigungen für den Ereignisendpunkt (oder beides) hinzugefügt werden.

Juval Lowy ist als Softwarearchitekt bei IDesign tätig. Er bietet WCF-Training und Beratung zur WCF-Architektur an. Sein neuestes Buch heißt Programming WCF Services Third Edition (O’Reilly, 2010). Er ist zudem Microsoft Regional Director für das Silicon Valley. Sie erreichen Juval Lowy unter www.idesign.net.