「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。
ソフテックだより(発行日順)のページへ
ソフテックだより 技術レポート(技術分野別)のページへ
ソフテックだより 現場の声(シーン別)のページへ
私はWindowsアプリケーション開発を担当する機会が多い入社15年超の社員です。今回のソフテックだよりでは、Windowsアプリケーション開発における『WCF』を利用したTCP/IP通信機能の実現方法についてご紹介させていただきます。
WCFとは『Windows Communication Foundation』の略称で、サービス指向アーキテクチャ(SOA)の構築モデルで分散システムを実現するためにMicrosoft が策定した通信フレームワークになります。
WCFは下記の特徴があります。
WCFが登場する前にも複数の分散システムを実現させる技術(通信プロトコル)はありましたが、それぞれ実装方法が異なっていました。WCFではそれらを同一のプログラミングモデル(サービス指向)で実装させることができるようになりました。
今回のソフテックだよりでご紹介するテーマでは、Microsoft Visual Studioで提供(.NET Framework 3から導入)されるWCFサービス(ホスト)とWCFクライアント アプリケーションを作成するための仕組み(フレームワーク)を使って、TCP/IP通信でホスト・クライアント間の通信を容易に実現させる方法に絞ってご紹介させていただきます。
対応手順としては下記の順に対応していきますので、対応手順に沿ってご説明していきます。
図1. WCFサービス・クライアント アプリケーション対応イメージ
まず最初にWCFサービス アプリケーション(TCP/IPサーバ)を作成します。
対応ポイントとしては、下記になります。
今回は、WcfTestServiceという名前で日時データを取得/設定するサービスを提供するサンプルプログラムコード(Windowsコンソールアプリケーションとして作成)を例にご説明いたします。
最初にWCFサービス アプリケーション(ホスト)が提供するサービス定義をインターフェースで定義します。
WCFサービス(インターフェース)で独自データ型(クラス or 構造体)を使用する場合は、定義したデータ型に下記属性を付与します。
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サービスの定義
WCFホストサービス処理(WCFサービス インターフェースを継承するクラス)を作成します。
⇒ 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ホストサービスの実装
アプリケーション メインエントリポイントにWCFホストプロセスを実装します。
※
WCFエンドポイントの設定(ホストプロセスの通信設定情報(Address/Biding))は、アプリケーション構成ファイル(app.config)で指定する方法もありますが、今回はユーザ入力したIPアドレス+通信ポートでサービスを提供できるようにプログラムコードで指定する形にしています。
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サービス アプリケーション」を作成できました。
次いで「WCFクライアント アプリケーション」を作成します。
画面操作でWCFサービスアプリケーションが提供するサービス要求を行えるようにサンプルプロジェクトの方はWindowsフォーム アプリケーションで作成した例でご説明します。
図5. サービス参照の追加
本登録操作によって、WCFサービスが提供するサービスproxyクラスが自動生成(今回の例では"ServiceRefrence.WcfTestServiceClient"という名称クラス)されます。
尚、サービス定義が変更になる場合は、サービス参照の更新で更新できます。
図6. サービス参照の更新
WCFサービス要求は、自動生成されたproxyクラスを使って参照設定で追加したコンポーネント・ライブラリと同じ感覚(追加された参照クラスインスタンスを生成して、提供I/Fをコールするイメージ)で実装することができます。
サンプルプログラムでは下記のような画面イメージで、WCFサービスで提供されるサービス要求を行うように機能実装を行った例をご紹介します。
図7. WCFクラアント アプリケーション画面イメージ
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サービス要求を行う実装例
WCFを使ってTCP/IP通信によるサービス(サーバ)・クラアイント アプリケーションのプロセス間通信を行う実現方法についてご紹介させていただきました。 今回ご紹介させていただいたような方法(用途)でWCFを利用した時のメリット・デメリットについて触れたいと思います。
下記内容がメリットとして挙げられます。
下記内容がデメリット(注意点)として挙げられます。
分散システムにおいて異なるPC間で各種情報をやり取りする場合や、サーバ・クライアントPC間で各種要求処理を実現するのにTCP/IP通信(ソケット通信)を利用するケースは多いと思います。
今回ご紹介させていただいたWCFを活用することで、サーバ・クライアント アプリケーション間の通信処理(パケットデータの解析処理など)の手間を減らせる点に最もメリットを感じます。
また、Microsoft Visual Studioで提供されるWCFフレームワークが、複数の開発言語(C#/VisalBasic.NET/ASP.NET/WPF)に対応している点も利用用途の幅が広がります。
(例えば、WCFサービス アプリケーションをASP.NETで開発してWebサービス(IIS)で公開するようなことも可能ですので、別の機会にご紹介テーマとして取り上げさせていただくかもしれません。)
今回のソフテックだよりで取り上げさせていただいた内容が、Windowsアプリケーション開発時の手助けになれば幸いです。
(M.S.)
関連ページへのリンク
関連するソフテックだより