返り値を必要としない、重いサービスを呼び出すときにOneWayオペレーションは便利です。OneWayオペレーションを呼び出すと、クライアントはオペレーションの終了を待機せずに処理を続行します。通常は、OneWayオペレーションを使用したとしても、オペレーションが完了しない限りプロキシクラスを閉じることはできません。OneWayオペレーションを使用したとしてもWCFではクライアントからWCFサービスへの呼び出しが成功したことを確認する必要があるためです。OneWayオペレーションの完了を待機せずに接続を閉じる回避作として、リライアブルセッションを使用する方法があります。リライアブルセッションの構成サンプルはこちら

確認環境です。接続はwsHttpBindingを使用しています。

  • Windows Vista Enterprise(クライアント、サービス同一PC上)
  • 開発環境 Visual Studio 2008 Professional (英語版)
  • .NET 3.5

1.ソリューションの作成

Visual Studio 2008を起動して、空のソリューションを作成します。ソリューション名はWCFSample014とします。

2.WCFサービスの作成

2.1 プロジェクトの作成

ソリューションを右クリックしてプロジェクトを新規作成します。プロジェクトのテンプレートでWCF Service Library を選択して、プロジェクト名をWCFService.LongTimeServiceとしてOKボタンをクリックします。

2.2 プログラムの編集

プロジェクトを作成時に作成されたApp.configとService1.cs,IService1.csを削除する。

次に、プロジェクトを右クリック→[Add New Item]を選択して新規項目を追加します。表示されるAdd New ItemダイアログでWCF Serviceを選択して、ファイル名をHeavyServiceと入力し、Addボタンでファイルを作成します。

IHeavyService.csを開いてサービスコントラクトIHeavyServiceを次のように編集します。OperationContract.IsOneWayをtrueにすることで、ProcessHeavyServiceをOneWayオペレーションとして定義することができます。

namespace WCFSample.LongTimeService
{
    [ServiceContract(Namespace="http://handcraft.wcfsample.org/2008/05/17", Name="HeavyService")]
    public interface IHeavyService
    {
        /// <summary>
        /// 引数で指定されたミリ秒間スリープする
         /// </summary>
        /// <param name="msec">スリープするミリ秒時間</param>
        [OperationContract(IsOneWay=true)]
        void ProcessHeavyService(int msec);
    }
}

HeavyService.csを開いて、サービスクラスHeavyServiceを実装します。

namespace WCFSample.LongTimeService
{
    [ServiceBehavior()]
    public class HeavyService : IHeavyService
    {
        #region IHeavyService Members

        public void ProcessHeavyService(int msec)
        {
            System.Threading.Thread.Sleep(msec);
        }

        #endregion
    }
}

3 WCFホストプロジェクトの作成

ソリューションを右クリックして、WPFアプリケーションを新規作成します。プロジェクト名はWCFSample.LongTimeWCFHostとします。プログラムの内容はWCFサンプル再作成(ブラッシュアップ) で作成したものとほぼ同じですが、再掲します。

3.1 ファイルの編集

最初に、WCFService.LongTimeServiceをプロジェクトの参照に追加します。プロジェクト作成時に作成されたWindow1.xamlをLongTimeHost.xamlに変更し、App.xaml内のStartupUriをLongTimeHost.xamlに変更します。

<Application x:Class="WCFSample.LongTimeWPFHost.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="LongTimeHost.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

 3.2 プログラムの作成

LongTimeHost.xamlを編集します。内容はWCFサンプル再作成(ブラッシュアップ) で作成したものと同じです。

<Window x:Class="WCFSample.LongTimeWPFHost.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" SizeToContent="WidthAndHeight">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Button x:Name="btnStart" Grid.Row="0" Grid.Column="0" Content="Start" Margin="15" Width="50" Click="btnStart_Click"  />
        <Button x:Name="btnStop" Grid.Row="0" Grid.Column="1" Content="Stop" Margin="15" Width="50" Click="btnStop_Click" IsEnabled="False"/>
        <Label x:Name="lblState" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Content="サービスは開始されていません." />

    </Grid>
</Window>

次にコードビハインドファイルLongTimeHost.xaml.csを編集します。

namespace WCFSample.LongTimeWPFHost
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        ServiceHost _host = null;
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            _host = new ServiceHost(typeof(WCFSample.LongTimeService.HeavyService));
            _host.Open();
            lblState.Content = "サービス中...";
            btnStart.IsEnabled = false;
            btnStop.IsEnabled = true;
        }

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            _host.Close();
            lblState.Content = "サービス停止";
            btnStop.IsEnabled = false;
            btnStart.IsEnabled = true;
        }
    }
}

3.3 構成ファイルの編集

プロジェクトを右クリック→[Add]→[New Item]を選択して、ファイルの種類にApplication Configuration Fileを選択肢、構成ファイル名をApp.configとして新規作成します。App.configを右クリックしてWCF Service Configuration Editorで編集します。

Editor上でServiceを新規作成し、次のようにウィザードを進めます。

  • サービスのタイプはWCFSample.LongTimeService.HeavyService
  • ・コントラクトはWCFSample.LongTimeService.IHeavyService
  • 通信プロトコルはHttp
  • 互換性のプロトコル選択画面でAdvanced Web Services interoperability, Simplex communicationを選択
  • Addressをhttp://localhost:8056/LongTimeService
  • 確認画面(下図)で内容を確認してFinishボタンクリック

上書き保存し、構成ファイルを見ると、bindingにws2007HttpBindingが設定されているのでwsHttpBindingに変更して完了します。

編集完了後のApp.configは次のようになります。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <services>
            <service name="WCFSample.LongTimeService.HeavyService">
                <endpoint address="http://localhost:8056/LongTimeService" binding="wsHttpBinding"
                    bindingConfiguration="" contract="WCFSample.LongTimeService.IHeavyService" />
            </service>
        </services>
    </system.serviceModel>
</configuration>

3.4 動作確認

この時点で動作確認をして見ます。WCFService.LongTimeWPFHostをスターツアッププロジェクトに設定してデバッグ実行を行うと、httpプロトコルを使用して、ポートを使用するアクセス権がないために例外が発生します。Administrator権限でコマンドプロンプトを起動し次のコマンドを入力します。(コマンドプロンプトのアイコンを右クリックRun As Administratorで起動)

netsh http add urlacl url=http://+:8056/LongTimeService user=マシン名orドメイン名\ユーザ名

Vista以外での設定方法は次のサイト参照
http://msdn.microsoft.com/ja-jp/library/ms733768.aspx

設定後動作サービスを起動できることを確認します。

4.クライアントプロジェクトの作成

4.1 プロキシクラスの作成

Visual Studio 2008のコマンドプロンプトを起動します。WCFService.LongTimeService.dllからメタ情報を作成し、プロキシクラスを作成します。dllのあるフォルダまで移動して、コマンドを入力し、メタファイルを作成します。

svcutil WCFSample.LongTimeService.dll

 

作成されたサービスのメタファイルからプロキシを作成します。次のコマンドを入力します。

svcutil /namespace:*,WCFSample.Client handcraft.wcfsample.org.2008.05.17.wsdl *.xsd /out:Proxy.cs

 

4.2 WCFクライアントプロジェクトを作成

ソリューションを右クリックして、プロジェクトを新規作成します。プロジェクトテンプレートにConsole Applicationをクリックして、プロジェクト名を
WCFService.LongTimeConsoleClientとして新規作成する。System.ServiceModel.dllの参照をプロジェクトに追加します。

新規作成されたプロジェクトを右クリックして、既存の項目追加で4.1で作成したProxy.csを追加します。プロキシクラス作成時に同時に作成されたoutput.configもApp.configに名前を変えて、プロジェクトに追加します。App.configを開くと、デフォルト値がいろいろ設定されているので、削除して、編集します。今回はバインディング構成のデフォルト値を変更していないので、直接以下のように編集しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
              <binding name="DefaultBinding_HeavyService">
                
              </binding>
            </wsHttpBinding>
        </bindings>
        <client>
          <endpoint binding="wsHttpBinding" bindingConfiguration="DefaultBinding_HeavyService"
              contract="WCFSample.Client.HeavyService" name="DefaultBinding_HeavyService" address="http://localhost:8056/LongTimeService" >
          </endpoint>
        </client>
    </system.serviceModel>
</configuration>

4.3 プログラムの作成

Program.csを開いてMainメソッドを修正します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WCFSample.Client;

namespace WCFSample.LongTimeConsoleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("キーを入力して下さい");
            Console.ReadLine();
            try
            {
                HeavyServiceClient proxy = new HeavyServiceClient("DefaultBinding_HeavyService");
                Console.WriteLine("1回目の呼び出し開始");
                proxy.ProcessHeavyService(10000);
                Console.WriteLine("1回目の呼び出し完了");
                Console.WriteLine("2回目の呼び出し開始");
                proxy.ProcessHeavyService(10000);
                Console.WriteLine("2回目の呼び出し完了");
                proxy.Close();
            }
            catch (Exception ex)
            {
                System.Console.WriteLine(ex.Message);
            }
            Console.Write("プログラムが終了しました。キーを入力してください。");
            Console.ReadLine();
        }
    }
}

4.4 動作確認その1

プロジェクトをデバッグ実行します。デバッグ実行すると、2回目の呼び出し開始から2回目の呼び出し完了が表示されるまで10秒くらいかかり、プログラムが終了しました..のメッセージが表示されるまでさらに10秒かかると思います。2つの理由からです。

理由その1

2回目の呼び出し開始から2回目の呼び出し完了まで10秒かかるのは、サービスクラスのデフォルトのInstanceContextModeがPersessionであるため、1回目のProcessHeavyServiceメソッドを呼び出した後2回目のProcessHeavyServiceメソッドを呼び出しても、1回目のProcessHeavyServiceの実行が完了するまで、2回目のProcessHeavyServiceがブロックされるためです。

理由その2

2回目の呼び出し完了が表示されてから、プログラムが終了しました...が表示されるのに10秒程度かかるのは、proxy.Close()メソッドは2回目に呼び出したProcessHeavyServiceが完了するまで、ブロックされるためです。OneWayサービスの場合でも、サービスが実行されたことを確認する(応答メッセージを受け取る)まで、接続を閉じることができないためです。

4.5 動作確認その2

InstanceContextModeをPersessionにしたまま、理由その1を回避する方法の1つとして、サービスコントラクトのConcurrencyModeをConcurrencyMode.Multipleにする方法があります。以下設定例。

namespace WCFSample.LongTimeService
{
    [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple)]
    public class HeavyService : IHeavyService
    {
      ...
    }
}

実行を確認すると、2回目呼び出し完了まではすぐに表示されるようになりますが、プログラムが終了しました...が表示されるまで20秒位かかるようになります。

!!てちょっとまってとか思いました?私は思いましたよ。

ConcurrencyModeがMultipleとなっているのに、20秒かかるのはおかしいです。それぞれのProcessHeavyServiceメソッドでは10秒スリープするので、並行して呼び出しが実行されれば、10秒位かかってプログラムが完了するはずです。私も動かしてみてとまどいました。理由は画面上で(WPF上で)WCFサービスを作成しOpenしているためです。ServiceBehaviorにはUseSynchronizationContextというデフォルトがtrueのプロパティが定義されています。このプロパティがtrueの場合、サービスをOpenしたときにスレッドに同期コンテキストが存在する場合(今回の場合WPFを作成したときに同期コンテキストが作成されます)、サービスをOpenしたスレッド上でサービスメソッドが実行されるようになるようです。そのため、1回目,2回目の呼び出しともにUIスレッド上で実行されることになり、最初の呼び出しが完了後、2回目のサービスメソッドが実行されます。今回のプログラムはUIを更新するわけではないので、サービスがオープンされたUIスレッド上でサービスメソッドが実行される必要はありません。そこで、以下のようにUseSynchronizationContextをfalseにします。

namespace WCFSample.LongTimeService
{
    [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple, UseSynchronizationContext=false)]
    public class HeavyService : IHeavyService
    {
        ...
    }
}

 ふたたび実行すると、今度は10秒ほどで処理が完了しました。

MSDNマガジンにWCFにおける同期コンテキストの情報が掲載されていたので、リンクを掲載しておきます。

WCFでの同期コンテキスト
http://msdn.microsoft.com/ja-jp/magazine/cc163321.aspx

4.6 動作確認その3(最後)

最後に理由2を解決するためにWS-ReliableMessagingを使用するように構成しなおします。WS-ReliableMessagingを有効にする方法はWCF Sample 013: Reliable Session(リライアブルセッション)を使用する。 を参照して下さい。構成結果は次のようになります。

WCFSample.LongTimeWCFHostの構成ファイル

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="LongTimeWSBindingConfig" bypassProxyOnLocal="false">
                    <reliableSession enabled="true" />
                </binding>
            </wsHttpBinding>
        </bindings>
        <services>
            <service name="WCFSample.LongTimeService.HeavyService">
                <endpoint address="http://localhost:8056/LongTimeService" binding="wsHttpBinding"
                    bindingConfiguration="LongTimeWSBindingConfig" contract="WCFSample.LongTimeService.IHeavyService" />
            </service>
        </services>
    </system.serviceModel>
</configuration>

WCFService.LongTimeConsoleClientの構成ファイル

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="DefaultBinding_HeavyService">
                    <reliableSession enabled="true" />
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
          <endpoint binding="wsHttpBinding" bindingConfiguration="DefaultBinding_HeavyService"
              contract="WCFSample.Client.HeavyService" name="DefaultBinding_HeavyService" address="http://localhost:8056/LongTimeService" >
          </endpoint>
        </client>
    </system.serviceModel>
</configuration>

 今度は呼び出し10秒を待たずにプログラムが終了します。(最初の呼び出しが開始されるまでに接続を確立する時間が数秒かかるようです。)

説明は以上です。間違い、お気づきの点等ありましたら連絡ください。