HOME > ソフテックだより > 第291号(2017年10月4日発行) 技術レポート「WindowsアプリケーションにおけるWCFを利用したTCP/IP通信のご紹介」

「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。

ソフテックだより(発行日順)のページへ
ソフテックだより 技術レポート(技術分野別)のページへ
ソフテックだより 現場の声(シーン別)のページへ


ソフテックだより 第291号(2017年10月4日発行)
技術レポート

「WindowsアプリケーションにおけるWCFを利用したTCP/IP通信のご紹介」

1. はじめに

私はWindowsアプリケーション開発を担当する機会が多い入社15年超の社員です。今回のソフテックだよりでは、Windowsアプリケーション開発における『WCF』を利用したTCP/IP通信機能の実現方法についてご紹介させていただきます。

2. WCFとは

WCFとは『Windows Communication Foundation』の略称で、サービス指向アーキテクチャ(SOA)の構築モデルで分散システムを実現するためにMicrosoft が策定した通信フレームワークになります。

WCFは下記の特徴があります。

  • サービス指向 (サービス指向アーキテクチャ(SOA)でデータ送受信にWebサービスを使用)
  • 相互運用性 (WSDL、XSDの形式でサービスメタデータを公開できる)
  • 複数のメッセージパターン
  • セキュリティ (メッセージを暗号化してセキュリティを保護できる)
  • 機能拡張性 (サービス動作をカスタマイズできる多数のエントリポイント)

WCFが登場する前にも複数の分散システムを実現させる技術(通信プロトコル)はありましたが、それぞれ実装方法が異なっていました。WCFではそれらを同一のプログラミングモデル(サービス指向)で実装させることができるようになりました。

今回のソフテックだよりでご紹介するテーマでは、Microsoft Visual Studioで提供(.NET Framework 3から導入)されるWCFサービス(ホスト)とWCFクライアント アプリケーションを作成するための仕組み(フレームワーク)を使って、TCP/IP通信でホスト・クライアント間の通信を容易に実現させる方法に絞ってご紹介させていただきます。

対応手順としては下記の順に対応していきますので、対応手順に沿ってご説明していきます。

(1)
WCFサービス アプリケーションの作成
(2)
WCFクライアント アプリケーションの作成

WCFサービス・クライアント アプリケーション対応イメージ
図1. WCFサービス・クライアント アプリケーション対応イメージ

3. WCFサービス アプリケーションの作成

まず最初にWCFサービス アプリケーション(TCP/IPサーバ)を作成します。
対応ポイントとしては、下記になります。

(1)
WCFサービスの定義
(2)
WCFホストサービスの実装
(3)
WCFホストプロセスの実装

今回は、WcfTestServiceという名前で日時データを取得/設定するサービスを提供するサンプルプログラムコード(Windowsコンソールアプリケーションとして作成)を例にご説明いたします。

3.1 WCFサービスの定義 (コントラクト)

最初にWCFサービス アプリケーション(ホスト)が提供するサービス定義をインターフェースで定義します。

(a) サービス・オペレーション コントラクト

サービス定義したインターフェースには、下記属性を付与します。

  • System.ServiceModel.ServiceContract
  • System.ServiceModel.OperationContract

 

(b) データコントラクト

WCFサービス(インターフェース)で列挙型を使用する場合は、定義した列挙型に下記属性を付与します。

  • System.Runtime.Serialization.DataContract
  • System.Runtime.Serialization.EnumMember

WCFサービス(インターフェース)で独自データ型(クラス or 構造体)を使用する場合は、定義したデータ型に下記属性を付与します。

  • System.Runtime.Serialization.DataContract
  • System.Runtime.Serialization.DataMember
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace WcfTestHost
{
    /// <summary>
    /// WFTテストサービス インターフェース
    /// </summary>
    /// <remarks>サービスコントラクト</remarks>
    ServiceContract
    public interface IWcfTestService
    {
        /// <summary>
        /// 日時データ取得
        /// </summary>
        /// <returns>日時</returns>
        [OperationContract]
        DateTime GetData();
        
        /// <summary>
        /// 日時データ設定
        /// </summary>
        /// <param name="data">[in] 日時</param>
        OperationContract
        void SetData(DateTime data);
        
        /// <summary>
        /// 日時データリセット
        /// </summary>
        /// <returns>データリセット後の日時</returns>
        OperationContract
        DateTime ResetData();
        
        /// <summary>
        /// 日時データオフセット
        /// </summary>
        /// <param name="param">[in] 日時オフセット値</param>
        /// <returns>データオフセット後の日時</returns>
        OperationContract
        DateTime OffsetData(OffsetParam param);
    }
    
    /// <summary>
    /// データオフセット種類 列挙型
    /// </summary>
    /// <remarks>データコントラクト</remarks>
    [DataContract(Name = "OffsetType")]
    public enum OffsetType
    {
        /// <summary></summary>
        EnumMember
        Year = 0,
        /// <summary></summary>
        EnumMember
        Month = 1,
        /// <summary></summary>
        EnumMember
        Day = 2,
        /// <summary></summary>
        EnumMember
        Hour = 3,
        /// <summary></summary>
        EnumMember
        Minute = 4,
        /// <summary>< /summary>
        EnumMember
        Second = 5
    }
    
    /// <summary>
    /// データオフセットパラメタ 構造体
    /// </summary>
    /// <remarks>データコントラクト</remarks>
    DataContract
    public struct OffsetParam
    {
        /// <summary>データオフセット種類</summary>
        /// <remarks>データメンバ</remarks>
        DataMember
        public OffsetType Type;
        
        /// <summary>データオフセット値</summary>
        /// <remarks>データメンバ</remarks>
        DataMember
        public int Value;
    }
}

図2. WCFサービスの定義

3.2 WCFホストサービスの実装

WCFホストサービス処理(WCFサービス インターフェースを継承するクラス)を作成します。

(a) サービス実行動作の指定

下記属性を付与することでサービス実行動作を指定します。

  • System.ServiceModel.ServiceBehavior

 ⇒ InstanceContextMode プロパティ … Single (サービスを単一オブジェクトで使用)
⇒ ConcurrencyMode プロパティ … Single (サービスをシングルスレッドで処理)

※ 今回のサンプルプログラムでは、複数クライアント側からアクセスする情報が同一となるように上記の設定としています。(設定方法によっては、サービスオブジェクトをクライアントのセッション単位(マルチ管理)とすることも可能です。)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace WcfTestHost
{
    /// <summary>
    /// WFCテストサービス クラス
    /// </summary>
    /// <remarks>WCFテストサービス インターフェース(サービスコントラクタ)継承</remarks>
    [ServiceBehavior(
        InstanceContextMode = InstanceContextMode.Single, 
        ConcurrencyMode = ConcurrencyMode.Single)]
    public class WcfTestService : IWcfTestService 
    {
        /// <summary>日時データ</summary>
        DateTime _Data = DateTime.Now;
        
        /// <summary>
        /// 日時データ取得
        /// </summary>
        /// <returns>日時</returns>
        public DateTime GetData()
        {
            return _Data;
        }
        
        /// <summary>
        /// 日時データ設定
        /// </summary>
        /// <param name="data">[in] 日時</param>
        public void SetData(DateTime data)
        {
            // コンソール出力
            Console.WriteLine("[SetData] {0}", data.ToString("yyyy/MM/dd HH:mm:ss"));
            // 日時データへセット
            _Data = data;
        }
        
        /// <summary>
        /// 日時データリセット
        /// </summary>
        /// <returns>データリセット後の日時</returns>
        public DateTime ResetData()
        {
            // 日時データを初期化 (現在日時を適用)
            _Data = DateTime.Now;
            // コンソール出力
            Console.WriteLine("[ResetData] {0}", _Data.ToString("yyyy/MM/dd HH:mm:ss"));
            return _Data;
        }
        
        /// <summary>
        /// 日時データオフセット
        /// </summary>
        /// <param name="param">[in] 日時オフセット値</param>
        /// <returns>データオフセット後の日時</returns>
        public DateTime OffsetData(OffsetParam param)
        {
            try
            {
                // 日時データへオフセット値を反映
                //  ⇒ データオフセット種類によってオフセット値の適用単位を変える
                switch (param.Type)
                {
                    case OffsetType.Year:   //年
                        _Data = _Data.AddYears(param.Value); break;
                    case OffsetType.Month:  //月
                        _Data = _Data.AddMonths(param.Value); break;
                    case OffsetType.Day:    //日
                        _Data = _Data.AddDays(param.Value); break;
                    case OffsetType.Hour:   //時
                        _Data = _Data.AddHours(param.Value); break;
                    case OffsetType.Minute: //分
                        _Data = _Data.AddMinutes(param.Value); break;
                    case OffsetType.Second: //秒
                    default:
                        _Data = _Data.AddSeconds(param.Value); break;
                }
                // コンソール出力
                Console.WriteLine("[OffsetData] {0}:{1} => {2}", 
                    param.Type, param.Value, _Data.ToString("yyyy/MM/dd HH:mm:ss"));
            }
            catch
            {
                // コンソール出力 (エラー)
                Console.WriteLine("[OffsetData] {0}:{1} => Error:{2}", 
                    param.Type, param.Value, _Data.ToString("yyyy/MM/dd HH:mm:ss"));
            }
            
            return _Data;
        }
    }
}

図3. WCFホストサービスの実装

3.3 WCFホストプロセスの実装

アプリケーション メインエントリポイントにWCFホストプロセスを実装します。


WCFエンドポイントの設定(ホストプロセスの通信設定情報(Address/Biding))は、アプリケーション構成ファイル(app.config)で指定する方法もありますが、今回はユーザ入力したIPアドレス+通信ポートでサービスを提供できるようにプログラムコードで指定する形にしています。

(a) サービスエンドポイント作成

通信方式はTCP/IPサーバ・クライアント通信としたいので、NetTcpBindingクラスでサービスエンドポイントを登録します。
… "net.tcp://[***]/WcfTestService" のTCP通信アドレス情報で公開されます。
※ [***]: 指定(アプリ起動後にキー入力)したIPアドレス:通信ポート

(b) WSDLでサービスメタデータを公開

クライアントアプリケーションで、WCFサービスインターフェースを自動生成できるようにWSDLでサービスメタデータを公開するように設定します。
… "http://localhost:8000/WcfTestService/mex" のHTTPアドレスで公開されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace WcfTestHost
{
    class Program
    {
        /// <summary>
        /// アプリケーションのメインエントリポイント
        /// </summary>
        static void Main(string[] args)
        {
            try
            {
                // ホストサービス アドレス入力待機
                Console.WriteLine("[IPアドレス:ポート]を指定してください。");
                string strHostAddress = Console.ReadLine();
                
                // サービスエンドポイント生成
                var ntcpBinding = new NetTcpBinding(SecurityMode.None);
                ntcpBinding.OpenTimeout = new TimeSpan(0, 0, 10);
                ntcpBinding.SendTimeout = new TimeSpan(0, 0, 5);
                ntcpBinding.ReceiveTimeout = new TimeSpan(0, 0, 10);
                ServiceHost svcHost = new TimeSpan(typeof(WcfTestService));
                svcHost.AddServiceEndpoint(
                    typeof(IWcfTestService), ntcpBinding, 
                    string.Format("net.tcp://{0}/WcfTestService", strHostAddress));
                
                // サービス動作でWSDLファイル(サービスI/F仕様)を公開
                //  ⇒ クライアント側でproxyクラスが自動生成できるようになる
                ServiceMetadataBehavior smbMex = new ServiceMetadataBehavior();
                smbMex.HttpGetEnabled = true;
                smbMex.HttpGetUrl = new Uri("http://localhost:8000/WcfTestService/mex");
                svcHost.Description.Behaviors.Add(smbMex);
                svcHost.AddServiceEndpoint(
                    ServiceMetadataBehavior.MexContractName,
                    MetadataExchangeBindings.CreateMexHttpBinding(),
                    smbMex.HttpGetUrl);
                
                // サービス開始
                svcHost.Open();
                Console.WriteLine("サービスを開始しました。");
                
                // 入力を待機
                Console.WriteLine("[Enter]キー入力でサービス終了します。");
                Console.ReadLine();
                
                // サービス終了
                Console.WriteLine("サービスを終了します。");
                svcHost.Abort();
                svcHost.Close();
            }
            catch (Exception exp)
            {
                // エラー表示
                Console.WriteLine("サービス操作に失敗しました。");
                Console.WriteLine(exp.Message);
                Console.WriteLine();
                
                // 入力を待機
                Console.WriteLine("[Enter]キー入力で終了します。");
                Console.Read();
            }
        }
    }
}

図4. WCFホストプロセスの実装

上記のような対応の流れで、「WCFサービス アプリケーション」を作成できました。

4. WCFクライアント アプリケーションの作成

次いで「WCFクライアント アプリケーション」を作成します。

画面操作でWCFサービスアプリケーションが提供するサービス要求を行えるようにサンプルプロジェクトの方はWindowsフォーム アプリケーションで作成した例でご説明します。

4.1 サービス参照の追加

「WCFサービス アプリケーション」を起動&サービス開始(IPアドレス+通信ポート指定)した状態で、[プロジェクト(P)→サービス参照の追加(S)...]でWCFサービスアプリケーションが提供するサービスを以下の手順でプロジェクトに追加登録します。
  • アドレス … "http://localhost:8000/WcfTestService/mex" 指定した後、[移動(G)]ボタン押下
  • 名前空間 … "ServiceRefrence" 指定して[OK]ボタン

サービス参照の追加
図5. サービス参照の追加

本登録操作によって、WCFサービスが提供するサービスproxyクラスが自動生成(今回の例では"ServiceRefrence.WcfTestServiceClient"という名称クラス)されます。

尚、サービス定義が変更になる場合は、サービス参照の更新で更新できます。

サービス参照の更新
図6. サービス参照の更新

4.2 画面操作によるWCFサービス要求を行う処理の実装

WCFサービス要求は、自動生成されたproxyクラスを使って参照設定で追加したコンポーネント・ライブラリと同じ感覚(追加された参照クラスインスタンスを生成して、提供I/Fをコールするイメージ)で実装することができます。
サンプルプログラムでは下記のような画面イメージで、WCFサービスで提供されるサービス要求を行うように機能実装を行った例をご紹介します。

WCFクラアント アプリケーション画面イメージ
図7. WCFクラアント アプリケーション画面イメージ

  • [開始]チェックONでタイマ起動 → タイマ処理で定期的にGetData要求行う
    (WCFサービスとの接続が確立していなければ接続を行う)
  • [リセット]ボタンでResetData()要求を行う
  • [実行]ボタンでOffsetData(...)要求を行う
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text; using System.Windows.Forms;
using System.ServiceModel;

namespace WcfTestClient
{
    /// <summary>
    /// メインフォーム クラス
    /// </summary>
    public partial class FormMain : Form
    {
        /// <summary>WCFサービス アドレス書式</summary>
        const string SERVER_URI = "net.tcp://{0}/WcfTestService";
        /// <summary>サービスクライアント インスタンス</summary>
        ServiceReference.WcfTestServiceClient _Client = null;
        
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public FormMain()
        {
            InitializeComponent();
            
            // オフセットデータ種類 コンボボックス初期化
            DataTable dtOffsetType = new DataTable("OffsetType");
            {
                dtOffsetType.Columns.Add("Display", typeof(string));
                dtOffsetType.Columns.Add("Value", typeof(ServiceReference.OffsetType));
                foreach (var item in Enum.GetValues(typeof(ServiceReference.OffsetType)))
                {
                    var drAdd = dtOffsetType.NewRow();
                    drAdd[0] = item.ToString();
                    drAdd[1] = item;
                    dtOffsetType.Rows.Add(drAdd);
                }
            }
            this.cmbOffsetType.DisplayMember = "Display";
            this.cmbOffsetType.ValueMember = "Value";
            this.cmbOffsetType.DataSource = dtOffsetType;
        }
        
        /// <summary>
       /// 画面ロード イベント
        /// </summary>
        private void FormMain_Load(object sender, EventArgs e)
        {
            this.cmbOffsetType.SelectedValue = ServiceReference.OffsetType.Second;
            this.CloseComm();
        }
        
        /// <summary>
        /// 画面クローズド イベント
        /// </summary>
        private void FormMain_Load(object sender, EventArgs e)
        {
            this.CloseComm();
        }
        
        /// <summary>
        /// 接続チェック タイマイベント
        /// </summary>
        private void timPolling_Tick(object sender, EventArgs e)
        {
            // 接続チェック
            this.CheckComm();
        }
        
        /// <summary>
        /// [開始]チェック イベント
        /// </summary>
        private void chkStart_CheckedChanged(object sender, EventArgs e)
        {
            // 開始中はIPアドレス:通信ポート設定の変更不可
            this.txtIpAndPort.ReadOnly = this.chkStart.Checked;
            
            // 接続チェックタイマの開始・停止
            if (this.chkStart.Checked == false) 
            {
                this.CloseComm();
            }
            else
            {
                this.timPolling.Start();
            }
        }
        
        /// <summary>
        /// [リセット]ボタン イベント
        /// </summary>
        private void btnReset_Click(object sender, EventArgs e)
        {
            try
            {
                // リセットデータ要求
                this.txtData.Text = _Client.ResetData().ToString("yyyy/MM/dd HH:mm:ss");
            }
            catch
            {
                // 要求異常のため接続状態をチェック
                this.CheckComm();
            }
        }
        
        /// <summary>
        /// [オフセット]ボタン イベント
        /// </summary>
        private void btnOffset_Click(object sender, EventArgs e) 
        {
            try

            {
                // オフセットパラメタ作成
                var opParam = new ServiceReference.OffsetParam();
                opParam.Type = (ServiceReference.OffsetType)this.cmbOffsetType.SelectedValue;
                opParam.Value = (int)this.numOffsetValue.Value;
                
                // オフセットデータ要求
                this.txtData.Text = _Client.OffsetData(opParam).ToString("yyyy/MM/dd HH:mm:ss");
            }
            catch
            {
                // 要求異常のため接続状態をチェック
                this.CheckComm();
            }
        }
        
        /// <summary>
        /// WCFテストサービスとの接続チェック モジュール
        /// </summary>
        private void CheckComm()
        {
            try
            {
                // WCFテストサービス 定期問い合わせタイマ停止
                Cursor.Current = Cursors.WaitCursor;
                this.timPolling.Stop();
                
                // WCFテストサービス と未接続の場合は接続する
                if ((_Client == null) ||
                    (_Client.State == CommunicationState.Faulted))
                {
                    _Client = new ServiceReference.WcfTestServiceClient(
                        new NetTcpBinding(SecurityMode.None),
                        new EndpointAddress(string.Format(SERVER_URI, this.txtIpAndPort.Text)));
                    
                    _Client.Open();
                }
                
                // WCFテストサービス:GetData要求
                this.txtData.Text = _Client.GetData().ToString("yyyy/MM/dd HH:mm:ss");
                this.btnReset.Enabled = true;
                this.btnOffset.Enabled = true;
                this.timPolling.Interval = 200;
            }
            catch
            {
                // WCFテストサービスとの接続異常
                this.txtData.Text = "切断中";
                this.btnReset.Enabled = false;
                this.btnOffset.Enabled = false;
                this.timPolling.Interval = 1000;
            }
            finally
            {
                // WCFテストサービス 定期問い合わせタイマ再開
                Cursor.Current = Cursors.Default;
                this.timPolling.Start();
            }
        }
        
        /// <summary>
        /// WCFテストサービスとの接続切断 モジュール
        /// </summary>
        private void CloseComm()
        {
            // WCFテストサービス 停止状態
            this.timPolling.Stop();
            this.txtData.Text = "";
            this.btnReset.Enabled = false;
            this.btnOffset.Enabled = false;
            this.timPolling.Interval = 1000;
            
            // WCFテストサービス と接続中の場合は切断
            if (_Client != null)
            {
                if ((_Client.State != CommunicationState.Created) &&
                    (_Client.State != CommunicationState.Faulted))
                {
                    _Client.Close();
                }
                _Client = null;
            }
        }
    }
}

図8. WCFサービス要求を行う実装例

5. WCFフレームワーク利用時のメリット・デメリット(注意点)

WCFを使ってTCP/IP通信によるサービス(サーバ)・クラアイント アプリケーションのプロセス間通信を行う実現方法についてご紹介させていただきました。 今回ご紹介させていただいたような方法(用途)でWCFを利用した時のメリット・デメリットについて触れたいと思います。

5.1 メリット

下記内容がメリットとして挙げられます。

(1) サービス(サーバ)⇔クライアント間の通信処理を意識する必要がない

ご紹介させていただいた内容から、従来のTCP/IP通信(ソケット通信)で実装が必要な通信処理(サーバ・クライアント間でやり取りするパケットデータの解析処理)を意識することなくプロセス間通信を実現できるメリットを感じていただけたのではないでしょうか?
サービス⇔クライアント間でやり取りするサービス要求種類(I/F種類)の数が多くなるほど、こちらの恩恵を強く感じることができます。

 

(2) クライアント側はホストが提供するサービス要求を容易 且つ 確実に行うことができる

「4.WCFクライアント アプリケーションの作成」でご紹介したように、WCFクライアント アプリケーション側ではWCFホストサービスが提供するサービス要求(I/F)を自動生成されたproxyクラスを用いて確実に要求することが可能です。
(1)のメリットと重複しますが、従来のTCP/IP通信(ソケット)に付きまとう通信パケットの作成/解析処理での実装ミス(不具合)の心配から解放されます。

 

(3) テスト用アプリケーションを準備した確認が容易

WCFサービス定義(コントラクト)さえ一致していれば良いので、テスト目的のWCFサーバ・クライアント アプリケーションを作成して組み合わせ確認を行うような事も容易に対応可能です。

5.2 デメリット(注意点)

下記内容がデメリット(注意点)として挙げられます。

(1) クライアント同時接続数について

クライアントOSでWCFサービス アプリケーションを稼働させる場合、クライアント同時接続数は10セッションまでになり、注意が必要です。
また、WCFクライアント アプリケーションからの同時接続数を任意にコントロールできない点もデメリットとして挙げられます。

 

(2) サービス・クライアント間のセッション保持期間について

WCFホストプロセスで実装においてサービスエンドポイントに指定したバインディング情報の受信タイムアウト時間経過(クライアントからの要求がない)するとホスト側でクライアントとの通信セッションは自動で終了します。
ご紹介した「WCFクライアント アプリケーション」サンプルプログラムのように、WFCサービスと接続したセッションを保持して使いまわす(要求の度に接続処理を行いたくない)場合は、定期的にWCFサービス要求を行う必要があります。

 

(3) 通信データ量について(注意点)

WCFホストプロセス実装時に通信データ量の制限値(規定値は65536バイト)を指定することができます。その他にもWebメッセージ(SOAP)でやり取りされる配列型データの長さ(要素数)、文字列型データの長さ(文字列長)といった制限がありますので、サービス⇔クライアント間でやり取りされるデータ量/構造によってはこれらの制限値を意識(適宜設定)する必要があります。

6. さいごに

分散システムにおいて異なるPC間で各種情報をやり取りする場合や、サーバ・クライアントPC間で各種要求処理を実現するのにTCP/IP通信(ソケット通信)を利用するケースは多いと思います。
今回ご紹介させていただいたWCFを活用することで、サーバ・クライアント アプリケーション間の通信処理(パケットデータの解析処理など)の手間を減らせる点に最もメリットを感じます。

また、Microsoft Visual Studioで提供されるWCFフレームワークが、複数の開発言語(C#/VisalBasic.NET/ASP.NET/WPF)に対応している点も利用用途の幅が広がります。
(例えば、WCFサービス アプリケーションをASP.NETで開発してWebサービス(IIS)で公開するようなことも可能ですので、別の機会にご紹介テーマとして取り上げさせていただくかもしれません。)

今回のソフテックだよりで取り上げさせていただいた内容が、Windowsアプリケーション開発時の手助けになれば幸いです。

(M.S.)

[参考URL]
・Windows Communication Foundation ・基本的なWCFプログラミング
 

関連ページへのリンク

関連するソフテックだより

ページTOPへ