次の方法で共有


基礎

検出を使用して新しい WCF を発見する

Juval Lowy

コード サンプルのダウンロード

Microsoft .NET Framework 3.5 で実現できるすべての Windows Communication Foundation (WCF) 呼び出しには、共通する 2 つの制約があります。1 つ目は、サービスに割り当てられたポートまたはパイプが使用可能でなければならないことです。アプリケーション開発者や管理者は、このようなポートまたはパイプを文字どおり推測または予約することができる必要があります。2 つ目は、クライアントは、サービス エンドポイントのアドレス、ポート番号とサービス コンピューターの両方、またはパイプ名を、"演繹的に" 知る必要があることです。

サービスが、使用可能な任意のアドレスを使用できるとしたら、それはすばらしいことです。その場合、クライアントは、実行時にそのアドレスを検出する必要があります。実は、この検出がどのように行われるかを既定する、業界標準に基づくソリューションが存在します。単純に "検出" と呼ばれるこのソリューション (およびそのサポート メカニズム) がこのコラムのテーマです。また、このコラムでは、いくつかの役に立つツールやヘルパー クラスもご紹介します。これらのソース コードは、https://code.msdn.microsoft.com/mag2010WCF (英語) で入手することができます。

アドレス検出

検出には、ユーザー データグラム プロトコル (UDP) が使用されます。UDP は伝送制御プロトコル (TCP) と違ってコネクションレス プロトコルであり、パケットの送信者と受信者の間に直接の接続は必要ありません。クライアントは、UDP を使用して、指定された種類のコントラクトをサポートしているすべてのエンドポイントを探す検出要求をブロードキャストします。こうした要求は、サービスがサポートしている専用の検出エンドポイントによって受信されます。検出エンドポイントの実装によって、指定されたコントラクトをサポートしているサービス エンドポイントのアドレスがクライアントに返されます。クライアントは、サービスを検出すると、通常の WCF 呼び出しの場合と同様に、引き続き、検出されたサービスを呼び出します。このシーケンスを図 1 に示します。

図 1 UDP 経由のアドレス検出

画像: UDP 経由のアドレス検出

Metadata Exchange (MEX) エンドポイントと同様に、WCFは、次のような UdpDiscoveryEndpoint 型の標準的な検出エンドポイントを提供します。

public class DiscoveryEndpoint : ServiceEndpoint

{...}

public class UdpDiscoveryEndpoint : DiscoveryEndpoint

{...}

サービスは、サポートする動作のコレクションに ServiceDiscoveryBehavior を追加することにより、このエンドポイントをホストに実装させることができます。次のように、プログラムによってこれを行うことができます。

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

図 2 は、サービス構成ファイルを使用して検出エンドポイントと検出動作を追加する方法を示しています。

図 2 構成ファイルで検出エンドポイントを追加

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

動的アドレス

検出は、サービス ホストが具体的にどのようにサービス エンドポイントを定義するかに依存しません。しかし、クライアントが検出を使用してサービス アドレスを見つけることが期待される場合はどうでしょうか。その場合、サービスは、使用可能な任意のポートまたはパイプに基づいて、実行時に (動的に) エンドポイント アドレスを構成することができます。

動的アドレスを使用して自動化を行うために、AvailableIpcBaseAddress および AvailableTcpBaseAddress という 2 つのプロパティを持つ次のような DiscoveryHelper 静的ヘルパー クラスを記述しました。

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

AvailableIpcBaseAddress を実装するのは簡単です。一意の名前が付けられたパイプであれば何でも目的を果たすので、このプロパティは、新しいグローバル一意識別子 (GUID) を使用してパイプに名前を付けます。AvailableTcpBaseAddress は、開いているポート 0 を通じて使用可能な TCP ポートを見つけることによって実装されます。

図 3 は、AvailableTcpBaseAddress の使い方を示しています。

図 3 動的アドレスの使用

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>

必要なものがサービスの動的ベース アドレスである場合は、図 3 のコードは決して完ぺきではありません。このコードだけでは不十分で、(構成ファイルで、またはプログラムによって) 検出を追加する必要があるからです。
次のように定義された EnableDiscovery ホスト拡張を使用して、これらのステップを合理化することができます。

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

次のように、EnableDiscovery を使用する際は、プログラムによる措置や構成ファイルは必要ありません。

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

サービスのエンドポイントがまだホストによって定義されていない場合、EnableDiscovery によって既定のエンドポイントが追加されます。また、EnableDiscovery は、既定で、ベース アドレス上の MEX エンドポイントをサービスに追加します。

クライアント側のステップ

クライアントは、次のような DiscoveryClient クラスを使用して、指定されたコントラクトをサポートするすべてのサービスのすべてのエンドポイント アドレスを検出します。

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

論理的には、DiscoveryClient は検出エンドポイントへのプロキシです。すべてのプロキシと同様に、クライアントは、プロキシのコンストラクターにターゲット エンドポイントに関する情報を提供する必要があります。クライアントは、構成ファイルを使用してエンドポイントを指定したり、プログラムによって標準的な UDP 検出エンドポイントを提供したりすることによってこれを行うことができます。それ以上の詳細 (アドレスやバインディングなど) は必要ないためです。続いて、クライアントは Find メソッドを呼び出し、FindCriteria (次のコード参照) のインスタンスを通じて、検出するコントラクトの種類を指定します。

public class FindCriteria

{

   public FindCriteria(Type contractType);
   //More members

}

Find は FindResponse のインスタンスを返します。このインスタンスには、検出されたすべてのエンドポイントのコレクションが格納されます (次のコード参照)。

public class FindResponse

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

各エンドポイントは、次のような EndpointDiscoveryMetadata クラスによって表されます。

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

EndpointDiscoveryMetadata の主なプロパティは Address です。このプロパティには、最終的に、検出されたエンドポイント アドレスが格納されます。図 4 は、クライアントが、エンドポイント アドレスを検出しサービスを呼び出すために、これらの型をどのように組み合わせて使用することができるかを示しています。

図 4 エンドポイントの検出と呼び出し

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();

図 4 には、特筆すべき問題がいくつかあります。

クライアントは目的のコントラクトをサポートしているエンドポイントを複数検出する可能性がありますが、どのエンドポイントを呼び出すかを決定するロジックを備えていません。クライアントは単純に、返されたコレクションの中の最初のエンドポイントを呼び出します。

検出はアドレスのみを対象としています。どのバインディングをサービスの呼び出しに使用するかに関する情報はありません。図 4 では、TCP バインディングの使用が単純にハードコーディングされています。クライアントは、サービス アドレスを検出する必要があるたびに、このような細かいステップを何度も繰り返さなければなりません。

検出には時間がかかります。既定では、Find は、サービスが UDP 検出要求に応答するのを 20 秒間待ちます。待ち時間がこれほど長いと、検出は、呼び出しを絶え間なく大量に実行する多くのアプリケーションでは間違いなく使用に適さないものになってしまいます。このタイムアウトを短くすることもできますが、そうすると、サービスを 1 つも検出できなかったりすべては検出できなかったりする危険性があります。DiscoveryClient は非同期の検出を提供しますが、サービスを実行する前にそのサービスを呼び出す必要があるクライアントにとっては、これは役に立ちません。

このコラムでは、これらの問題への対処方法をいくつかご紹介します。

スコープ

検出の使用は、クライアントとクライアントが検出するサービスとの関係が少しゆるいことを意味します。これにより、新たな問題が浮上します。クライアントは、適切なエンドポイントを検出できたことをどのようにして知るのでしょうか。互換性のあるエンドポイントが複数検出された場合、クライアントはどのエンドポイントを呼び出せばよいのでしょうか。

クライアントが検出の結果をフィルター処理するのに役立つなんらかのメカニズムが必要なのは明らかです。スコープはまさにこのようなものです。スコープは、単に、エンドポイントに関連付けられた有効な URL です。サービスは、1 つまたは複数のスコープを各エンドポイントに関連付けることができます。スコープは、アドレスと共に、検出要求への応答に含められます。それに対して、クライアントは、検出されたアドレスを見つかったスコープに基づいてフィルター処理することができます。または、そもそも、関連するスコープのみを見つけようとすることができます。

スコープは、検出をカスタマイズしたりアプリケーションに高度な動作を追加したりするのに非常に役立ちます。フレームワークや管理ツールを記述する場合は特にそうです。スコープの典型的な用途は、異なるアプリケーションのポリモーフィックなサービスをクライアントが識別できるようにすることです。しかし、このような状況はややまれです。私は、スコープは同じアプリケーション内でエンドポイントの種類を識別するのに便利だと感じています。

たとえば、あるコントラクトの実装が複数存在するとします。本稼動に使用される運用モードと、テストや分析に使用されるシミュレーション モードがあります。スコープを使用すると、クライアントは必要とされる適切な種類の実装を選択することができ、異なるクライアントが互いのサービスを使用するせいで互いに競合することはなくなります。同じクライアントが呼び出しのコンテキストに基づいて異なるエンドポイントを選択するようにすることもできます。プロファイル用、デバッグ用、診断用、テスト用、インストルメンテーション用などのエンドポイントを用意することができます。

ホストは、EndpointDiscoveryBehavior クラスを使用してエンドポイントごとにスコープを割り当てます。たとえば、すべてのエンドポイントに適用するには、次のように既定のエンドポイント動作を使用します。

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

サービスの種類に基づいて個々にスコープを適用するには、エンドポイントごとに明示的に動作を割り当てます ( 図 5 参照)。

図 5 動作の明示的な割り当て

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

次のように、1 つの検出動作で複数のスコープを列挙することもできます。

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

あるエンドポイントに関連付けられたスコープが複数存在する場合は、クライアントがスコープの照合に基づいてエンドポイントの検出を試みた際、スコープのうちすべてではなく少なくとも 1 つが一致する必要があります。

クライアントがスコープを使用する方法は 2 つあります。1 つ目は、次のように、検索条件にスコープを追加するという方法です。

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

これで、Find メソッドは、互換性があり、なおかつこのスコープを列挙するエンドポイントのみを返すようになります。クライアントが複数のスコープを追加する場合は、Find は、列挙されたスコープをすべてサポートしているエンドポイントのみを返します。エンドポイントは、Find に渡されるもの以外のスコープもサポートしている場合があります。

スコープを使用する 2 つ目の方法は、次のようにして、FindResponse で返されるスコープを検査するというものです。

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

このようなスコープは当該のエンドポイントによってサポートされているすべてのスコープであり、追加のフィルター処理に役立ちます。

検出の基数

検出を使用する場合は必ず、クライアントは私が "検出の基数" と呼んでいるもの (いくつのエンドポイントが検出されたか、および (検出された場合は) どのエンドポイントを呼び出すか) に対処する必要があります。基数には次のようないくつかのケースがあります。

  • エンドポイントが検出されなかった場合。この場合、クライアントはサービスがないことに対処する必要があります。これは、サービスが使用不可能な他の WCF クライアントと同様です。
  • 互換性のあるエンドポイントが 1 つだけ検出された場合。これは圧倒的に最も一般的なケースです。クライアントは単純に、サービスの呼び出しという次の段階に移ります。
  • 複数のエンドポイントが検出された場合。この場合、クライアントは理論上は 2 つの選択肢を持ちます。1 つ目は、検出されたエンドポイントをすべて呼び出すというものです。これは、パブリッシャーがサブスクライバーに対してイベントを発生させる場合に該当し (後述)、有効なシナリオです。2 つ目の選択肢は、検出されたエンドポイントのすべてではなくいくつか (または 1 つだけ) を呼び出すというものです。このシナリオは現実的ではないと思います。どのエンドポイントを呼び出すかを決定するロジックをクライアントに配置しようとすると、
システム全体にわたる結合度が高くなりすぎます。そうすると、実行時の検出という概念 (検出されたエンドポイントであれば何でも目的を果たすという概念) そのものが無効になってしまいます。望ましくないエンドポイントが検出される可能性があるとしたら、検出の使用は設計上の選択として不適切であり、静的なアドレスをクライアントに提供した方がよいことになります。

クライアントがエンドポイントを 1 つだけ検出することを期待する場合 (基数が 1 の場合)、クライアントは Find に、そのエンドポイントを見つけたらすぐに呼び出し元に戻るよう指示する必要があります。そうすることによって、検出の待ち時間が大幅に短縮され、ほとんどのケースに適した長さになります。

クライアントは、次のように FindCriteria の MaxResults プロパティを使用して基数を構成することができます。

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

次のような Discovery​Helper.DiscoverAddress<T> ヘルパー メソッドを使用して、基数が 1 のケースを合理化することができます。

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

DiscoverAddress<T> を使用すると、図 4 を次のように書き換えることができます。

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

検出を合理化する

これまでは、クライアントは使用するバインディングをハードコーディングする必要がありました。しかし、サービスが MEX エンドポイントをサポートしている場合、クライアントは、MEX エンドポイント アドレスを検出し、続いて、エンドポイント アドレスだけでなく使用するバインディングも取得するためにメタデータを取得し処理することができます。MEX エンドポイントの検出に役立つように、FindCriteria クラスには次のように静的メソッド CreateMetadataExchangeEndpointCriteria が用意されています。

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

このシーケンスを合理化するには、次のような DiscoveryFactory.CreateChannel<T> メソッドを使用します。

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

CreateChannel<T> を使用すると、図 4 を次のように書き換えることができます。

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

CreateChannel<T> は、MEX エンドポイントの基数が 1 であること (つまり、ローカル ネットワークで、検出可能な MEX エンドポイントが 1 つだけ見つかること)、および指定された型パラメーター T をコントラクトとするエンドポイントが 1 つだけメタデータに含まれることを前提としています。

CreateChannel<T> は、エンドポイントのバンディングとアドレスの両方に MEX エンドポイントを使用します。(クライアントは実際のエンドポイントを見つけるのに検出エンドポイントを使用することはありませんが、) サービスは、MEX エンドポイントと検出エンドポイントの両方をサポートすることを期待されます。

目的のサービス コントラクトをサポートしているサービスが複数ある場合や MEX エンドポイントが複数ある場合に備えて、DiscoveryFactory には次のような CreateChannels<T> メソッドも用意されています。

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

CreateChannels<T> は、既定では、使用するバインディングをサービス エンドポイントのスキームから推測します。inferBinding が false の場合、CreateChannels<T> は MEX エンドポイントからバインディングを検出します。

CreateChannels<T> は、互換性のあるサービス エンドポイントまたは MEX エンドポイントの基数が 1 であることを前提としておらず、互換性のあるすべてのエンドポイントから成る配列を返します。

アナウンス

ここまでに紹介した検出メカニズムは、サービスの観点から見ると受動的なものです。クライアントが検出エンドポイントに照会し、サービスが応答します。この受動的なアドレス検出に代わるものとして、WCF は能動的なモデルを提供します。このモデルでは、サービスがサービスの状態をすべてのクライアントにブロードキャストし、サービスのアドレスを提供します。サービス ホストは、ホストが開かれたときに "hello" アナウンスをブロードキャストし、ホストが正常にシャットダウンしたときに "bye" アナウンスをブロードキャストします。ホストが異常終了した場合、"bye" アナウンスは送信されません。このようなアナウンスは、クライアントによってホストされた特別なアナウンス エンドポイントで受信されます (図 6 参照)。

図 6 アナウンス アーキテクチャ

画像: アナウンス アーキテクチャ

アナウンスは、ホスト レベルのメカニズムではなく個々のエンドポイント レベルのメカニズムです。ホストは、アナウンスするエンドポイントを選択することができます。各アナウンスには、エンドポイントのアドレス、スコープ、およびコントラクトが含まれています。

アナウンスはアドレス検出とは無関係です。ホストは検出エンドポイントをまったくサポートしていない場合もあり、検出動作は必須ではありません。一方、ホストは、検出エンドポイントのサポートとエンドポイントのアナウンスの両方に対応する場合もあります (図 6 参照)。

ホストは、エンドポイントを自動的にアナウンスすることができます。皆さんは、検出動作のクライアント アナウンス エンドポイントに関する情報を提供すればよいだけです。たとえば、構成ファイルを使用する場合は次のようにしてこれを行います。

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

私が作成した EnableDiscovery 拡張メソッドでも、検出動作にアナウンス エンドポイントが追加されます。

クライアントで使用するように、WCF では、アナウンス エンドポイントのあらかじめ用意された実装が AnnouncementService クラスによって提供されています (図 7 参照)。

図 7 WCF でのアナウンス エンドポイントの実装

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 
}

AnnouncementService は、同時アクセス用に構成されたシングルトンです。AnnouncementService は、アナウンスを受信するためにクライアントがサブスクライブすることができる 2 つのイベント デリゲートを提供します。クライアントは、シングルトン インスタンスを受け取る ServiceHost のコンストラクターを使用して AnnouncementService をホストする必要があります。これは、クライアントがインスタンスを操作しイベントをサブスクライブすることを可能にするために必要です。また、クライアントは、次のように、UDP アナウンス エンドポイントをホストに追加する必要があります。

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)
{...}

アナウンスの受信に関する重要事項が 1 つあります。クライアントは、コントラクトの種類にかかわらず、さらに言うと、アプリケーションやスコープにかかわらず、イントラネット内のすべてのサービスのすべての通知を受信します。クライアントは、適切なアナウンスをフィルターで除外する必要があります。

アナウンスを合理化する

次のように定義された AnnouncementSink<T> クラスを使用すると、アナウンスを利用するためにクライアントが実行する必要がある元のステップを大幅に単純化し改善することができます。

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

AnnouncementSink<T> は、図 7 のステップをカプセル化することによってアナウンス エンドポイントのホスティングを自動化します。AnnouncementSink<T> は内部で AnnouncementService のインスタンスをホストしますが、AnnouncementService の問題点を改善します。まず、AnnouncementSink<T> は通知用の 2 つのイベント デリゲートを提供します。元の AnnouncementService と違って、AnnouncementSink<T> はこの 2 つのデリゲートを同時に呼び出します。また、AnnouncementSink<T> は、続く任意のスレッドでアナウンスを受け取ることができるようにし、本当に同時実行を可能にするために、AnnouncementService の同期コンテキスト アフィニティを無効にします。

AnnouncementSink<T> はコントラクトの種類をフィルター処理し、互換性のあるエンドポイントが自身をアナウンスした場合にのみイベントを発生させます。クライアントが行う必要があるのは、通知の受信をいつ開始し、いつ終了するかを示すために、AnnouncementSink<T> を開き、閉じることだけです。

AnnouncementSink<T> は、AddressesContainer<T> という汎用アドレス コンテナーから派生します。

AddressesContainer<T> は、複数のアドレスを操作する必要がある場合はいつでも使用することができる、充実したアドレス管理ヘルパーコレクションです。AddressesContainer<T> は、いくつかの反復子、インデクサー、変換メソッド、およびクエリをサポートしています。

図 8 は、AnnouncementSink<T> の使い方を示しています。

図 8 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();
   } 
}

MEX Explorer

Programming WCF Services, Second Edition』(O’Reilly、2008 年) という著書の中で、私は、自分が MEX Explorer と呼んでいるツールをご紹介しました (図 9 参照)。MEX Explorer で MEX アドレスを指定すると、サービス エンドポイントの情報 (アドレス、バインディング プロパティ、およびコントラクト) が示されます。検出の導入により、私は MEX Explorer を改訂することができるようになりました。

図 9 MEX Explorer

画像: MEX Explorer

Clicking the Discover button triggers a discovery request for all MEX endpoints without any limit on cardinality.続いて、検出されたすべてのエンドポイントがツリーで視覚化されます。また、MEX Explorer は MEX エンドポイントのアナウンスを利用します。アナウンスを受けて、MEX Explorer の画面が更新され、新しいエンドポイントが表示されたり、もう実行されていないエンドポイントがツリーから削除されたりします。

検出駆動型のパブリッシュ - サブスクライブ パターン

2006 年 10 月号の記事「一方向呼び出し、コールバック、およびイベントに関して知っておく必要のあること」(英語) では、WCF でパブリッシュ - サブスクライブ パターンをサポートするためのフレームワークをご紹介しました。検出とアナウンスのメカニズムを使用して、パブリッシュ - サブスクライブ システムを実装するまた別の方法を提供することができます。

その記事でご紹介した手法と違って、検出ベースのソリューションは、サブスクライバーや管理者による明示的なステップが不要な唯一のパブリッシュ - サブスクライブ ケースです。検出を利用する場合、コードまたは構成ファイルで明示的にサブスクライブする必要はありません。これにより、システムの配置が大幅に簡単になり、パブリッシャーとサブスクライバーの両方が存在する場合は高い柔軟性が実現されます。追加の管理ステップやプログラミングなしで、サブスクライバーやパブリッシャーを容易に追加または削除することができます。

パブリッシュ - サブスクライブ システムで検出を利用する場合、サブスクライバーは、パブリッシュ - サブスクライブ サービスがサブスクライバーの検出またはイベント処理エンドポイントのアナウンス (またはこの両方) を行えるように、検出エンドポイントを提供することができます。

パブリッシャーはサブスクライバーを直接検出するべきではありません。直接検出すると、(基数がすべてのエンドポイントとなり) 発生するすべてのイベントで検出の待ち時間が生じる可能性があるためです。パブリッシャーは、サブスクライバーを直接検出するのではなく、パブリッシュ - サブスクライブ サービスを検出する必要があります。これは 1 回限りのごくわずかなコストで済みます。パブリッシュ - サブスクライブ サービスはシングルトンである必要があります (シングルトンの場合、基数が 1 なので迅速な検出が可能になります)。パブリッシュ - サブスクライブ サービスはサブスクライバーと同じイベント エンドポイントを公開するので、パブリッシャーからはメタサブスクライバーのように見えます。つまり、パブリッシュ - サブスクライブ サービスに対してイベントを発生させるには、実際のサブスクライバーに対してイベントを発生させるのと同じコードが必要だということです。

パブリッシュ - サブスクライブ サービスのイベント エンドポイントは、特別なスコープを使用する必要があります。このスコープは、パブリッシャーがサブスクライバーではなくパブリッシュ - サブスクライブ サービスを見つけることを可能にします。パブリッシュ - サブスクライブ サービスは、このスコープ指定されたイベント エンドポイントの検出をサポートするだけでなく、アナウンス エンドポイントも提供します。

パブリッシュ - サブスクライブ サービスは、すべてのサブスクライバーのリストを保持します。パブリッシュ - サブスクライブ サービスは、なんらかの継続的なバックグラウンド処理を使用して絶えずサブスクライバーの検出を試みることにより、このリストを最新の状態に保つことができます。また、パブリッシュ - サブスクライブ サービスのイベント エンドポイントを特別なスコープに関連付けると、パブリッシュ - サブスクライブ サービスがすべてのイベント エンドポイントを検出する際にパブリッシュ - サブスクライブ サービス自身を検出することが阻止されます。パブリッシュ - サブスクライブ サービスは、サブスクライバーを監視するためのアナウンス エンドポイントを提供することもできます。図 10 は、このアーキテクチャを示しています。

図 10 検出駆動型のパブリッシュ - サブスクライブ システム

画像: 検出駆動型のパブリッシュ - サブスクライブ システム

パブリッシュ - サブスクライブ サービス

独自のパブリッシュ - サブスクライブ サービスの配置に役立てていただくために、次のように定義された DiscoveryPublishService<T> を記述しました。

[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
}

皆さんが行う必要があるのは、独自のパブリッシュ - サブスクライブ サービスを DiscoveryPublishService<T> から派生させ、イベント コントラクトを型パラメーターとして指定することだけです。その後、FireEvent メソッドを呼び出してイベント コントラクトの操作を実装します。

たとえば、次のイベント コントラクトについて考えてみましょう。

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

図 11 は、DiscoveryPublishService<T> を使用して独自のパブリッシュ - サブスクライブ サービスを実装する方法を示しています。

図 11 パブリッシュ - サブスクライブ サービスを実装

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

DiscoveryPublishService<T> は、内部では、AddressContainer<T> から派生したクラスの 1 つである次のように定義された DiscoveredServices<T> を使用します。

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

DiscoveredServices<T> は、すべての検出されたサービスのできる限り最新の状態のリストを保持するように設計されており、検出したアドレスを基本クラスに格納します。DiscoveredServices<T> は進行中の検出をバックグラウンド スレッドに分離します。これは、検出されたアドレスの現在のリポジトリが必要な場合に役立ちます。

FireEvent メソッドは、メッセージ ヘッダーから操作名を抽出します。続いて、パブリッシュ - サブスクライブ スコープをサポートしていないすべてのサブスクライバーを探すためにサブスクライバー リストに照会します (自己検出を避けるため)。次に、FireEvent は、リストを一意のエントリの集合体にまとめます (これは、自身をアナウンスし検出可能でもあるサブスクライバーに対処するために必要です)。サブスクライバーごとに、FireEvent はアドレス スキームからバインディングを推測し、そのサブスクライバーに対して呼び出すプロキシを作成します。イベントのパブリッシュは、スレッド プールのスレッドを使用して同時に行われます。

独自のパブリッシュ - サブスクライブ サービスをホストするには、次のような、DiscoveryPublishService<T> の静的ヘルパー メソッド CreateHost<S> を使用します。

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

型パラメーター S は DiscoveryPublishService<T> の皆さん独自のサブクラスで、T はイベント コントラクトです。CreateHost<S> は、開く必要のあるサービス ホストのインスタンスを返します。

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

また、CreateHost<S> は使用可能な TCP ベース アドレスの取得とイベント エンドポイントの追加も行うので、構成ファイルは必要ありません。

パブリッシャー

パブリッシャーは、イベント サービスへのプロキシを必要とします。これには、次のような DiscoveryPublishService<T>.CreateChannel を使用します。

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

DiscoveryPublishService<T>.CreateChannel は、パブリッシュ - サブスクライブ サービスを検出し、検出されたサービスへのプロキシを作成します。基数が 1 なので、この検出は高速です。次のように、パブリッシャーのコードは簡単です。

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

サブスクライバーの実装に関しては、特別なことを行う必要はありません。サービスでイベント コントラクトをサポートし、イベント エンドポイントの検出とアナウンスのいずれか (または両方) を追加するだけです。

Juval Lowy は IDesign 社に所属するソフトウェア アーキテクトで、WCF のトレーニングとアーキテクチャ コンサルティングを行っています。最新の著書は、『Programming WCF Services, Third Edition』(O'Reilly、2010 年出版予定) です。シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は www.idesign.net (英語) です。