ちょっとWCFでデッドロック地獄に陥ったりふんだりけったりなので、自分の頭の中を整理するためにWCFのチュートリアルみたいなのを書いてみたいと思います。
あらかじめ言い訳をしておくと、僕がWCF関連の技術で使ったことがあるのは.Net Remotingだけで、それも数年前にちょっと触った程度です。ですから・・・たぶん勘違いところがあるかもしれませんのでそのときは教えてくださいね!
このブログでは主にXNAなどゲーム関連の技術について書いてきたので、WCFもそうなのかと思う人も居るかもしれません。僕が見た感じでは使える場合もあるし使えない場合もあるといったところでしょうか。WCFによる通信は単にTcpで行った場合よりもパフォーマンスは悪いはずなので、FPSのネット対戦にはまあ、向かないでしょう。しかし少々通信に時間がかかってもいいようなボードゲームなんかには向いているといえるかもしれません。まあその場合はXNAを使う必要はなく、WPFを使うことになるでしょうけどね。
WCFとは
WCF(Windows Communication Foundation)とは.Net Framework3.0の三本柱(WPF、WCF、WF)の一つで、サービス志向に基づいたアプリケーションを作るフレームワークです。要はネットワークを通じて別々のコンピュータが通信を行うことが出来るわけです。なんだかゲームに使えそうですね!
以前にも似たような技術として.Net Remotingがあったのですが、どうやら問題があるとか何とかでWCFがでたようです。.Net Remotingに無かったもの、つまり.Net RemotingとWCFの違いは、サービス志向とやらなのだそうです。サービス志向とはカプセル化を極めまくったものらしく、これでプログラムを書くと、通信に使うインターフェースをプログラマは強く意識することになります(.net Remotingではどれがリモートのオブジェクトなのかということはほとんど意識する必要はありませんでした)。
カプセル化を行うと普通は物事をあまり気にしなくていいようになるものですが、サービス志向の場合は違って、サーバー側が内部で.Net Frameworkを使っているということまで外部から隠さなければならないため、通信に使うインターフェースは限られた機能しか持たないようなものになり、結果としてプログラマはどれがリモートオブジェクトなのかを強く意識しなければいけなくなるようです。通信相手はC#じゃなくてJavaかもしれないということですね。例えば.Net Remotingでは何がローカルにあるオブジェクトで何がリモートにあるオブジェクトなのか気にする必要はありませんでしたが、WCFではインターフェースに属性を付けて、「これを使ってリモートオブジェクトと通信するんだ」ということを明示しなければいけません。どこでリモートと通信するのかを意識することによって無駄な通信を避けるという意味合いもあるそうです。
また、サーバーの内部を隠蔽、ということはメソッドの引数や戻り値に(そのままでは)独自のクラスを用いることが出来ません。クライアント側が使わないような型の引数や戻り値を使われては困るので、カスタムクラスを使う場合は、属性を使って明示しなければいけないのです。
ようするに、WCFではこれまでと違って、通信の境界面に注意を払うということですね。
簡単な例
思うにWCFには、まぁ他の技術もそうですが、簡単なソースコードの実例がかけています。というわけでここではWCFを使って足し算をするサービスを作って見ます。本当なら「このコンピューターを足し算するサーバーにして、あのコンピュータから2 + 3の計算をリクエストする」見たいなことをやるべきなのでしょうが、ここでは話を簡単にするために1台のパソコンで出来るようにしましょう。
サーバープログラムと、それにアクセスするクライアントプログラムの2つを作るので、2つプロジェクトを作るといいでしょう。プロジェクトを作ったらその両方に、Project -> Add ReferenceでSystem.ServiceModel.dllを追加します。WCFを使うにはこれが必要です。
コードはこんな具合です。
サーバー側
using System;
using System.ServiceModel;
namespace Server
{
[ServiceContract]
interface ICalculator
{
[OperationContract]
int Add(int a, int b);
}
class MyCalculator : ICalculator
{
public int Add(int a, int b)
{
return a + b;
}
}
class Program
{
static void Main()
{
ServiceHost serviceHost = new ServiceHost(typeof(MyCalculator));
serviceHost.AddServiceEndpoint(
typeof(ICalculator),
new NetTcpBinding(),
"net.tcp://localhost:8001/Calculator"
);
serviceHost.Open();
Console.WriteLine("サービスがオープンしました。終了するにはEnterを押してください。");
Console.ReadLine();
serviceHost.Close();
}
}
}
クライアント側
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
namespace Client
{
[ServiceContract]
interface ICalculator
{
[OperationContract]
int Add(int a, int b);
}
class Program
{
static void Main()
{
ICalculator remoteCalculator = new ChannelFactory<ICalculator>(
new NetTcpBinding(),
"net.tcp://localhost:8001/Calculator"
).CreateChannel();
Console.WriteLine("2 + 3 = " + remoteCalculator.Add(2, 3));
((IChannel)remoteCalculator).Close();
}
}
}
この2つをビルドしたら、Visual Studioのプロジェクトのフォルダから、まずサーバーのほうの実行ファイルを起動します。サービスがオープンすると"サービスがオープンしました。終了するにはEnterを押してください"と表示されるので、今度はクライアントのほうの実行ファイルを起動します。そうすると、"2 + 3 = 5"と表示されるはずです。
ここでは、サーバー側でもクライアント側でもICalculatorインターフェースを宣言しています。そしてそれやそのメソッドに属性を付けています。インターフェースにはServiceContract属性を、メソッドにはOperationContract属性を付けています。これらの属性を付けられると、WCFで通信に使う約束事、コントラクトを意味するようになります。これらの属性が付けられたもの(つまりコントラクト)以外は、外部から隠蔽されるわけです。ですからOperationContractを付け忘れたりすると、そのメソッドをWCFで呼び出すことは出来なくなります。
ここではこのコントラクトのインターフェース(ICalculator)をサーバーとクライアントの両方で、合わせて2度も定義しています。これをすると、厳密に言うと2つのICalculatorの型はCLR的には違うものとなるのですが、サービス志向の疎結合っぷりのおかげでWCFでは問題なく通信できます。ただ、「同じコードを2度も書くのは面倒だ」ということで、ICalculatorを別のファイルで一回だけ定義して1つのdllにして、それをサーバーとクライアント両方からコンパイル時に参照するという方法も使えます。
System.ServiceModel.ServiceContractAttribute属性
これがくっつけられたインターフェースはWCFのサービスであることを表します。
System.ServiceModel.OperationContractAttribute属性
これがくっつけられたメソッドがサービスの操作であることを表します。
System.ServiceModel.ServiceHostクラス
サービスを公開するのに使います。ミニサーバーみたいなものです。これを使わなくてもIISやWASを使えばサービスは公開できますが、このクラスを使うとプログラムから制御できますからね。
ここで使ったコンストラクタはこれです:
public ServiceHost
(
Type serviceType,
params Uri[] baseAddresses
)
serviceTypeは公開するサービスを実装したクラスのタイプです。
baseAddressesはここでは使いませんでしたが、AddServiceEndpointのaddressのベースとなるアドレスです。たとえばここに"net.tcp://localhost:8001/"を入れると、AddServiceEndpointのaddressは"Calculator"だけでよくなります。
これを呼んだ後に、serviceTypeによって実装されたサービスを公開するAddServiceEndpointメソッドを呼びます。
public ServiceEndpoint AddServiceEndpoint
(
Type implementedContract,
Binding binding,
string address
)
implementedContractはサービスのコントラクトを表すインターフェースのタイプです。
bindingは通信手段です。ここではゲームに向いてそうなTcpを使いましたが、HttpやMsmqを使う方法もあります。
addressはサービスを公開するアドレスです。
実際にサービスを開始するにはOpenメソッドを呼び、使い終わったらCloseメソッドを呼びます。
System.ServiceModel.ChannelFactory<TChannel>ジェネリッククラス
クライアント側のプログラムで使う、プロキシオブジェクトを生成するクラスです。サービスを使おうと思ったらこのクラスを使います。(TChannelはサーバー側で実装したコントラクトを表すインターフェースです)
コンストラクタ:
public ChannelFactory
(
Binding binding,
string remoteAddress
)
bindingは通信手段です。サーバー側でセットしたものと同じものを指定します。
remoteAddressはアクセスするサービスのアドレスです。これまたサーバー側でセットしたものと同じものを指定します。
インスタンスを生成したら、CreateChannelメソッドでプロキシを生成できます。
public TChannel CreateChannel()
この戻り値のオブジェクトはSystem.ServiceModel.Channels.IChannelインターフェースも実装しています。
プロキシを使い終わったら、キャストしてIChannel.Closeメソッドを呼びます。