忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。


WCFチュートリアル カスタム型を使う

WCFのコントラクトではそのままでは、引数や戻り値にカスタムなクラスを使うことが出来ません。クライアントからサーバーへ独自に定義したクラスを(そのままでは)送れませんし、サーバーからクライアントへ、独自に定義したクラスを(そのままでは)返せないのです。

× ダメなコントラクトの例(マネしないで!) ×
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    //Error!
    //(throws InvalidDataContractException)
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}


もちろん全くムリということではありません。だいたいInt32だってクラス(構造体)の一つなんですからね。ある型をWCFでの引数や戻り値に使うためには、いくつかの条件のうちの一つを満たしている必要があります。そのうちの最も基本的なのは、System.Serializable属性が付けられていることでしょう。この属性は、Int32、Single(float)、String、DateTimeなど、.netの多くの型につけられています(ですからこれらの型はそのままWCFのコントラクトに使用することが出来ます)

SerializableAttributeをつける

カスタムクラスをWCFで使えるようにする方法のひとつがこれです。つまり、作ったカスタムクラスにSerializableAttributeを付けるのです。SerializableAttributeをつけられた型は、リフレクションによりprivateだろうとpublicだろうとすべてのフィールドがシリアライズされ、ネットの向こう側に送られます(もちろん同じパソコンに送られる場合もありますけどね)。さっきの例を正しくするとこうなります。

コントラクト:
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Serializable]
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}

せっかくなのでサーバーとクライアントも書いておきます。サーバーのプロジェクトもクライアントのプロジェクトも、このコントラクトのプロジェクトを参照しています。

サーバー:
using System;
using System.ServiceModel;

using Contracts;

namespace Server
{
    class MyCalculator : ICalculator
    {
        public double GetArea(Rectangle rect)
        {
            return rect.Width * rect.Height;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            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;

using Contracts;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            ICalculator remoteService = new ChannelFactory(
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                ).CreateChannel();

            Console.WriteLine(
                "2 * 3 = "
                + remoteService.GetArea(new Rectangle{Width = 2, Height = 3})
                );

            ((IChannel)remoteService).Close();
        }
    }
}

実行するとクライアント側で"2 * 3 = 6"というふうに表示されるはずです。Rectangleオブジェクトがバイト列にシリアライズされ、サーバープログラムに送られているわけですね。

(Serializable属性を使う場合に限らず、このようにしてサービスとクライアントの間を飛び交うオブジェクトはちょうどC言語の構造体のようなものになります。つまり、メソッドを持ちませんし、値がそのままコピーされるのです。メソッドを持たないというのはどういうことかというと、データだけだということです。これは「データとその操作を一緒にまとめる」というオブジェクト指向に反しているように見えますが、WCFのサービスコントラクトが使う型はメソッドを持っていても仕方が無いですからね。また、サーバークライアント間を移動するときにデータが反対側でシリアライズされるという方法を取るので、片方のオブジェクトの値を変更してももう片方のオブジェクトには影響を与えません。参照型ではなくて値型みたいなもんなんですね。)


DataContractAttributeとDataMemberAttributeをつける

しかしこのSerializableAttributeを使う方法はWCFとしてはあまり勧められないのだそうです。WCFのうたうサービス志向にのっとっていないからでしょう。コントラクトはなるべく明示的であるべきということでしょうか・・・。それにまあ、SerializableAttributeはWCFより以前からあった属性ですしね。

WCF特有の方法には、DataContractAttributeDataMenberAttributeを使う方法があります。DataContractは型に付け、DataMemberはそのフィールドまたはプロパティに付けます。この2つは両方ともSystem.Runtime.Serialization名前空間内にあり、使うにはSystem.Runtime.Serialization.dllの参照を加える必要があります(意外ですね・・・・・・)。

さっきのSerializable属性を使った方法をこの方法で書き換えるとこうなります:

using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Runtime.Serialization.DataContract]
    public class Rectangle
    {
        [System.Runtime.Serialization.DataMember]
        public double Width;

        [System.Runtime.Serialization.DataMember]
        public double Height;
    }
}


さっきの方法と違うのはコントラクトだけです。サーバーとクライアントはそのまま。















拍手[0回]

PR

WCFチュートリアル かんたんなサーバーとクライアント

ちょっと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を使うにはこれが必要です。

AddingWCFComponent.jpg

コードはこんな具合です。
サーバー側
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メソッドを呼びます。




拍手[6回]