非同期プログラミング

WCF の代替としての非同期 TCP ソケット

James McCaffrey

コード サンプルのダウンロード

マイクロソフト テクノロジ環境でクライアント サーバー システムを作成する場合、Windows Communication Foundation (WCF) を使用するのが一般的です。WCF には、HTTP Web サービス、Web API、DCOM、AJAX Web テクノロジ、名前付きパイプ プログラミング、TCP ソケットそのものを使用するプログラミングなど、多くの代替がありますが、当然それぞれにメリットとデメリットがあります。しかし、開発作業、管理の容易さ、スケーラビリティ、パフォーマンス、セキュリティなど、さまざまな要因を考慮すると、多くの状況で WCF を使用するのが最も効率の良いアプローチです。

ただし、WCF はかなり複雑になることがあり、プログラミングの状況によってはかなりの作業が必要になることがあります。個人的な見解ですが、Microsoft .NET Framework 4.5 がリリースされる前の非同期ソケット プログラミングは難解すぎて、大半は使う気になれませんでした。しかし、C# 言語の新しい言語機能の await と async はその評価を変えるほど使いやすく、非同期クライアント サーバー システムにソケット プログラミングを使用することが以前よりも魅力的な選択肢となりました。今回は、.NET Framework 4.5 の新しい非同期機能を使用して、低レベルでパフォーマンスの高い非同期クライアント サーバー ソフトウェア システムを作成する方法について説明します。

今回の目的は、図 1 に示すデモ クライアント サーバー システムをご覧いただければ一目瞭然です。一番上のコマンド シェルでは、いくつか数値を受け取り、平均値と最小値を求める非同期 TCP ソケットベースのサービスを実行しています。中央は、数値 (3, 1, 8) の平均値を計算する要求を送信している Windows フォーム (WinForm) アプリケーションです。このクライアントは非同期に実行され、要求送信後サービスの応答を待機する間、[Say Hello] ボタンを 3 回クリックしたところで、アプリケーションから応答が返っています。

Demo TCP-Based Service with Two Clients図 1 デモ TCP ベースのサービスと 2 つのクライアント

図 1 の一番下は、動作中の Web アプリケーション クライアントで、(5, 2, 7, 4) の最小値を求める非同期要求を送信しています。スクリーンショットでははっきりと分かりませんが、サービスからの応答を待機している間も、Web アプリケーションはユーザーの入力に対応します。

ここからは、このサービス、WinForm クライアント、および Web アプリケーション クライアントのコーディング方法を示します。その過程で、ソケットを使用する際のメリットとデメリットについて説明します。今回は、少なくとも中級以上の C# プログラミング スキルがあることを前提にしていますが、非同期プログラミングの詳しい知識や経験は問いません。付属のコード ダウンロードに、図 1 に示す 3 つのプログラムの完成形のソース コードを含めています。メインとなる考え方が明確になるように、通常行う大半のエラー チェックは省略しています。

サービスを作成する

デモ サービスの全体構造を図 2 に示します (スペースを節約するために少し編集しています)。サービスを作成するには、必須の .NET Framework 4.5 がインストールされているコンピューターで Visual Studio 2012 を起動し、DemoService という名前の新しい C# コンソール アプリケーションを作成します。ソケットベースのサービスは明確に限定された機能を持つ傾向があるため、実際のシナリオではより内容を表す名前を使用することをお勧めします。

図 2 デモ サービス プログラムの構造

 

using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading.Tasks;
namespace DemoService
{
  class ServiceProgram
  {
    static void Main(string[] args)
    {
      try
      {
        int port = 50000;
        AsyncService service = new AsyncService(port);
        service.Run();
        Console.ReadLine();
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        Console.ReadLine();
      }
    }
  }
  public class AsyncService
  {
    private IPAddress ipAddress;
    private int port;
    public AsyncService(int port) { . . }
    public async void Run() { . . }
    private async Task Process(TcpClient tcpClient) { . . }
    private static string Response(string request)
    private static double Average(double[] vals) { . . }
    private static double Minimum(double[] vals) { . . }
  }
}

テンプレート コードがエディターに読み込まれたら、ソース コードの先頭にある using ステートメントを修正して、System.Net と System.Net.Sockets をインクルードします。次に、ソリューション エクスプローラー ウィンドウで、Program.cs ファイルの名前を ServiceProgram.cs に変更します。これにより Program クラスの名前が Visual Studio によって自動的に変更されます。サービスの最初の部分はシンプルです。

 

int port = 50000;
AsyncService service = new AsyncService(port);
service.Run();

サーバー上のソケットベースのカスタム サービスはそれぞれ重複しないポートを使用する必要があります。一般にポート番号 49152 ~ 65535 をカスタム サービスに使用します。ポート番号の競合を避けるのは困難な場合があり、システム レジストリの ReservedPorts エントリを使用して、サーバーのポート番号を予約できます。今回のサービスは、オブジェクト指向プログラミング (OOP) デザインを使用し、ポート番号を受け取るコンストラクターを利用してサービスのインスタンスを作成します。サービス ポート番号は固定されるため、ポート番号をパラメーターとして渡すのではなくハードコーディングしてもかまいません。Run メソッドには while ループが 1 つあります。このループ内でクライアント要求を受け取り、処理します。Enter キーが押されたことをコンソール シェルが認識するとループを終了します。

AsyncService クラスには 2 つのプライベート メンバー (ipAddress と port) があります。基本的にこの 2 つの値でソケットを定義します。コンストラクターはポート番号を受け取り、サーバーの IP アドレスをプログラムで決定します。パブリック Run メソッドでは、要求を受け取り、計算を行い、応答を送信します。Run メソッドから Process ヘルパー メソッドを呼び出し、Process ヘルパー メソッドから Response ヘルパー メソッドを呼び出します。Response メソッドは、Average ヘルパー メソッドと Minimum ヘルパー メソッドを呼び出します。

ソケットベースのサーバーを編成する方法はたくさんあります。デモで使用している構造は、モジュール性とシンプルさを兼ね備え、実践でもうまく機能します。

サービス コンストラクターと Run メソッド

ソケットベースのデモ サービスの 2 つのパブリック メソッドを図 3 に示します。コンストラクターは、ポート名を格納後、GetHostName メソッドを使用してサーバー名を特定してから、サーバーについての情報を含む構造体をフェッチします。AddressList コレクションは、IPv4 アドレスや IPv6 アドレスなど、さまざまなコンピューター アドレスを保持します。InterNetwork 列挙値は IPv4 アドレスを意味します。

図 3 サービスのコンストラクターと Run メソッド

public AsyncService(int port)
{
  this.port = port;
  string hostName = Dns.GetHostName();
  IPHostEntry ipHostInfo = Dns.GetHostEntry(hostName);
  this.ipAddress = null;
  for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
    if (ipHostInfo.AddressList[i].AddressFamily ==
      AddressFamily.InterNetwork)
    {
      this.ipAddress = ipHostInfo.AddressList[i];
      break;
    }
  }
  if (this.ipAddress == null)
    throw new Exception("No IPv4 address for server");
}
public async void Run()
{
  TcpListener listener = new TcpListener(this.ipAddress, this.port);
  listener.Start();
  Console.Write("Array Min and Avg service is now running"
  Console.WriteLine(" on port " + this.port);
  Console.WriteLine("Hit <enter> to stop service\n");
  while (true) {
    try {
      TcpClient tcpClient = await listener.AcceptTcpClientAsync();
      Task t = Process(tcpClient);
      await t;
    }
    catch (Exception ex) {
      Console.WriteLine(ex.Message);
    }
  }
}

今回のアプローチではサーバーに制約を設け、サーバーに最初に割り当てられた IPv4 アドレスだけを使用して要求をリッスンします。よりシンプルにするには、this.ipAddress = IPAddress.Any のようにメンバー フィールドを割り当てることによって、複数のアドレスのいずれかに送信された要求をサーバーが受け取れるようにします。

サービスの Run メソッドのシグネチャに async 修飾子を使用することで、メソッド本体では await キーワードを使用して非同期メソッドを呼び出すことになります。Run メソッドは、一般的な Task ではなく void を返しています。これは Run メソッドが Main メソッドから呼び出されるためです。Main メソッドはやや特殊で async 修飾子が許可されません。代わりに、Task 型を返す Run メソッドを定義し、service.Run().Wait のようにメソッドを呼び出すこともできます。

サービスの Run メソッドは、サーバーの IP アドレスとポート番号を使用して TcpListener オブジェクトのインスタンスを作成します。リスナーの Start メソッドは、指定されたポートの監視を開始し、接続要求を待機します。

メインの処理を行う while ループ内では、TcpClient オブジェクトを作成し、AcceptTcpClientAsync メソッドを使用して接続を待機します。TcpClient オブジェクトはインテリジェント ソケットと考えることができます。.NET Framework 4.5 より前は、BeginAcceptTcpClient を使用してカスタム非同期調整コードを記述する必要がありました。これは簡単ではありません。.NET Framework 4.5 には、メソッド名の最後に "Async" が付く新しいメソッドがたくさん追加されました。これらの新しいメソッドを、async キーワードと await キーワードと組み合わせることで、非同期プログラミングがはるかに容易になります。

Run メソッドでは、2 つのステートメントを使用して Process メソッドを呼び出しています。代わりに、ショートカット構文を使用して、Process メソッドを 1 つのステートメント await Process(tcpClient) で呼び出してもかまいません。

まとめると、サービスは TcpListener オブジェクトと TcpClient オブジェクトを使用して、ソケットそのものを使うプログラミングの複雑さをなくしています。また新しい AcceptTcpClientAsync メソッドを新しい async キーワードおよび await キーワードと併用して、非同期プログラミングの複雑さをなくします。Run メソッドでは、接続処理の設定と調整を行い、Process メソッドを呼び出して要求を処理し、Task が返されるのを待機するために 2 つ目のステートメントを呼び出します。

サービスの Process メソッドと Response メソッド

サービス オブジェクトの Process メソッドと Response メソッドを図 4 に示します。Process メソッドのシグネチャでは async 修飾子を使用し、Task 型を返します。

図 4 デモ サービスの Process メソッドと Response メソッド

private async Task Process(TcpClient tcpClient)
{
  string clientEndPoint =
    tcpClient.Client.RemoteEndPoint.ToString();
  Console.WriteLine("Received connection request from "
    + clientEndPoint);
  try {
    NetworkStream networkStream = tcpClient.GetStream();
    StreamReader reader = new StreamReader(networkStream);
    StreamWriter writer = new StreamWriter(networkStream);
    writer.AutoFlush = true;
    while (true) {
      string request = await reader.ReadLineAsync();
      if (request != null) {
        Console.WriteLine("Received service request: " + request);
        string response = Response(request);
        Console.WriteLine("Computed response is: " + response + "\n");
        await writer.WriteLineAsync(response);
      }
      else
        break; // Client closed connection
    }
    tcpClient.Close();
  }
  catch (Exception ex) {
    Console.WriteLine(ex.Message);
    if (tcpClient.Connected)
      tcpClient.Close();
  }
}
private static string Response(string request)
{
  string[] pairs = request.Split('&');
  string methodName = pairs[0].Split('=')[1];
  string valueString = pairs[1].Split('=')[1];
  string[] values = valueString.Split(' ');
  double[] vals = new double[values.Length];
  for (int i = 0; i < values.Length; ++i)
    vals[i] = double.Parse(values[i]);
  string response = "";
  if (methodName == "average") response += Average(vals);
  else if (methodName == "minimum") response += Minimum(vals);
  else response += "BAD methodName: " + methodName;
  int delay = ((int)vals[0]) * 1000; // Dummy delay
  System.Threading.Thread.Sleep(delay);
  return response;
}

Windows Communication Foundation (WCF) の代わりに低レベルのソケットを使用するメリットの 1 つは、診断用の WriteLine ステートメントを好きな場所に簡単に挿入できることです。デモでは、セキュリティ上の理由から clientEndPoint をダミーの IP アドレス値 123.45.678.999 に置き換えました。

以下は、Process メソッドの中でも重要な 3 行です。

 

string request = await reader.ReadLineAsync();
...
string response = Response(request);
...
await writer.WriteLineAsync(response);

1 つ目のステートメントは「要求の 1 行を非同期で読み取り、必要に応じて他のステートメントの実行を許可する」と解釈できます。要求文字列を取得したら、Response ヘルパーに渡します。次にその応答を要求元のクライアントに非同期で送り返します。

サーバーは、読み取り要求と書き込み応答のサイクルを繰り返します。このサイクルはシンプルですが、把握しておくべき注意点がいくつかあります。サーバーで書き込みを行わず、読み取りのみを行う場合、この半開状態を検出できません。サーバーで読み取りを行わずに、書き込みのみを行う場合 (大量のデータを応答する場合など)、クライアントがデッドロックに陥るおそれがあります。シンプルな社内のサービスでは読み取りと書き込みを繰り返す設計も容認されますが、クリティカルなサービスや一般に公開するサービスには使用しないことをお勧めします。

Response メソッドは要求文字列を受け取り、その要求を解析して応答文字列を計算します。ソケットベースのサービスでは、ある種のカスタム プロトコルを作成する必要があります。これはメリットでもあり、デメリットでもあります。今回の場合、要求が次のようになっていることを想定しています。

method=average&data=1.1 2.2 3.3&eor

つまり、サービスは「リテラル "method=" + 文字列 "average" または文字列 "minimum" + アンパサンド文字 ("&") + リテラル "data="」という入力データを想定します。実際の入力データは、スペースで区切る必要があります。この要求は「"&"+ リテラル "eor"」で終了します。"eor" は end-of-request (要求の終了) の略です。WCF と比較した場合のソケットベース サービスのデメリットは、複雑なパラメーター型をシリアル化するのが少し面倒な場合があることです。

このデモの例のサービス応答はシンプルで、数値配列の平均値または最小値を表す単なる文字列です。カスタム クライアント サーバーの多くは、サービス応答用になんらかのプロトコルを設計する必要があります。たとえば、"4.00" のような単純な応答を返す代わりに、"average=4.00" のように応答を返すことも考えられます。

Process メソッドは、例外が発生した場合に接続を閉じるという比較的大雑把なアプローチを使用します。代わりに、C# の using ステートメント (すべての接続が自動的に閉じられます) を使用して、Close メソッドの明示的呼び出しを削除することもできます。

Average ヘルパー メソッドと Minimum ヘルパー メソッドは次のように定義します。

private static double Average(double[] vals)
{
  double sum = 0.0;
  for (int i = 0; i < vals.Length; ++i)
    sum += vals[i];
  return sum / vals.Length;
}
private static double Minimum(double[] vals)
{
  double min = vals[0]; ;
  for (int i = 0; i < vals.Length; ++i)
    if (vals[i] < min) min = vals[i];
  return min;
}

デモ サービスに似たプログラム構造を使用するのであれば、ほとんどの場合、この時点でヘルパー メソッドからデータ ソースに接続し、データをフェッチします。低レベルのサービスのメリットは、データ アクセスのアプローチをきめ細かく制御できることです。たとえば、SQL からデータを取得する場合、従来の ADO.NET、Entity Framework などのあらゆるデータ アクセス手法を使用できます。

低レベルのアプローチのデメリットは、システムでのエラー処理の方法を明示的に決める必要があることです。ここでは、デモ サービスが要求文字列を正しく解析できなかった場合、有効な応答を (文字列として) 返す代わりに、サービスからエラー メッセージを返します。経験上、信頼できる一般原則はごくわずかです。それぞれのサービスにはカスタム エラー制御が必要です。

Response メソッドにはダミーの遅延を用意しています。

int delay = ((int)vals[0]) * 1000;
System.Threading.Thread.Sleep(delay);

この応答の遅延は要求の最初の数値を基に任意に決定され、サービスからの応答を遅らせて、WinForm クライアントと Web アプリケーション クライアントで、応答を待機している間の UI 応答のデモを可能にしています。

WinForm アプリケーション デモ クライアント

図 1 に示した WinForm クライアントを作成するには、Visual Studio 2012 を起動し、DemoFormClient という名前の新しい C# WinForm アプリケーションを作成します。既定では、Visual Studio は WinForm アプリケーションを複数のファイルにモジュール分割し、UI のコードとロジックのコードを分離します。付属のコード ダウンロードでは、モジュール分割された Visual Studio コードを 1 つのソース コード ファイルにリファクタリングしています。Visual Studio のコマンド シェルを起動し、csc.exe /target:winexe DemoFormClient.cs コマンドを実行して、アプリケーションをコンパイルします。C# のコンパイラは Visual Studio が起動します。

Visual Studio のデザイン ツールを使用して、ComboBox コントロール、TextBox コントロール、2 つの Button コントロール、ListBox コントロール、および 4 つの Label コントロールを追加します。ComboBox コントロールの Items コレクション プロパティに、文字列 "average" および文字列 "minimum" を追加します。button1 と button2 の Text プロパティをそれぞれ "Send Async" と "Say Hello" に変更します。次に、デザイン ビューで、button1 コントロールと button2 コントロールをダブルクリックして、イベント ハンドラーを登録します。図 5 に示すようにクリック ハンドラーを編集します。

図 5 WinForm デモ クライアントのボタンのクリック ハンドラー

private async void button1_Click(object sender, EventArgs e)
{
  try {
    string server = "mymachine.network.microsoft.com";
    int port = 50000;
    string method = (string)comboBox1.SelectedItem;
    string data = textBox1.Text;
    Task<string> tsResponse = 
      SendRequest(server, port, method, data);
    listBox1.Items.Add("Sent request, waiting for response");
    await tsResponse;
    double dResponse = double.Parse(tsResponse.Result);
    listBox1.Items.Add("Received response: " +
     dResponse.ToString("F2"));
  }
  catch (Exception ex) {
    listBox1.Items.Add(ex.Message);
  }
}
private void button2_Click(object sender, EventArgs e)
{
  listBox1.Items.Add("Hello");
}

button1 コントロールのクリック ハンドラーのシグネチャを変更して async 修飾子を指定します。このハンドラーでは、サーバーのコンピューター名 (文字列型) とポート番号をハードコーディングして設定しています。低レベルのソケットベースのサービスを使用すると自動検出メカニズムが存在しないため、クライアントはサーバー名または IP アドレスと、ポート情報にアクセスする必要があります。

重要なのは次のコード行です。

Task<string> tsResponse = SendRequest(server, port, method, data);
// Perform some actions here if necessary
await tsResponse;
double dResponse = double.Parse(tsResponse.Result);

SendRequest はプログラム定義の非同期メソッドです。この呼び出しを大まかに解釈すると、「文字列を返す非同期要求を送信し、完了したら "await tsResponse" ステートメントから実行を継続する」となります。これにより、アプリケーションは応答を待機している間に他の動作を実行できます。この応答は Task にカプセル化されるため、実際の文字列結果は Result プロパティを使用して抽出する必要があります。この文字列結果を、小数点以下 2 桁の書式に設定できるよう double 型に変換します。

以下のようにアプローチを変えることもできます。

string sResponse = await SendRequest(server, port, method, data);
double dResponse = double.Parse(sResponse);
listBox1.Items.Add("Received response: " + dResponse.ToString("F2"));

この場合、await キーワードを SendRequest の非同期呼び出しにインラインで設置しています。これにより、呼び出しコードが少し簡略化され、Task.Result を呼び出さずに結果の文字列をフェッチできます。インライン await 呼び出しを使用するか、独立したステートメントの await 呼び出しを使用するかはケースバイケースですが、一般に Task オブジェクトの Result プロパティの明示的な使用は避けることをお勧めします。

非同期作業の大半は、SendRequest メソッドで実行します (図 6 参照)。SendRequest は非同期なので、場合によっては SendRequestAsync や MySendRequestAsync という名前にする方が良いかもしれません。

図 6 WinForm デモ クライアントの SendRequest メソッド

private static async Task<string> SendRequest(string server,
  int port, string method, string data)
{
  try {
    IPAddress ipAddress = null;
    IPHostEntry ipHostInfo = Dns.GetHostEntry(server);
    for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
      if (ipHostInfo.AddressList[i].AddressFamily ==
        AddressFamily.InterNetwork)
      {
        ipAddress = ipHostInfo.AddressList[i];
        break;
      }
    }
    if (ipAddress == null)
      throw new Exception("No IPv4 address for server");
    TcpClient client = new TcpClient();
    await client.ConnectAsync(ipAddress, port); // Connect
    NetworkStream networkStream = client.GetStream();
    StreamWriter writer = new StreamWriter(networkStream);
    StreamReader reader = new StreamReader(networkStream);
    writer.AutoFlush = true;
    string requestData = "method=" + method + "&" + "data=" +
      data + "&eor"; // 'End-of-request'
    await writer.WriteLineAsync(requestData);
    string response = await reader.ReadLineAsync();
    client.Close();
    return response;
  }
  catch (Exception ex) {
    return ex.Message;
  }
}

SendRequest はサーバー名を表す文字列を受け取り、サービスのクラス コンストラクターと同じコード ロジックを使用して IP アドレスの名前解決から始めます。代わりに、await client.ConnectAsync(server, port) のように単純にサーバー名を渡す方がシンプルです。

サーバーの IP アドレスが決まったら、TcpClient インテリジェント ソケット オブジェクトのインスタンスを作成し、オブジェクトの ConnectAsync メソッドを使用してサーバーに接続要求を送信します。データをサーバーに送信するためのネットワーク StreamWriter オブジェクトと、サーバーからデータを受け取るための StreamReader オブジェクトを設定したら、サーバーが想定している形式で要求文字列を作成します。この要求は非同期に送受信され、文字列として返されます。

Web アプリケーション デモ クライアント

図 1 に示したデモ Web アプリケーション クライアントは 2 つの手順で作成します。まず、Visual Studio を使用してアプリケーションをホストする Web サイトを作成し、次にメモ帳を使用して Web アプリケーションをコーディングします。Visual Studio 2012 を起動して、http://localhost/ に DemoClient という名前で新しい C# 空の Web サイトを作成します。これにより、アプリケーションをホストするために必要なすべての IIS 設定が構成され、C:\inetpub\wwwroot\DemoClient\ に Web サイトと関連付けられる物理場所が作成されます。この過程で基本構成ファイル Web.config も作成されます。このファイルには、サイト内のアプリケーションが .NET Framework 4.5 の async 機能にアクセスするための情報が含まれています。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="false" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
</configuration>

次に、メモ帳を管理者特権で起動します。シンプルな ASP.NET アプリケーションを作成するとき、個人的な好みで Visual Studio の代わりにメモ帳を使用することがあります。そうすれば複数のファイルや不必要なサンプル コードを生成しないで、すべてのアプリケーション コードを 1 つの .aspx ファイルに保持できます。今回は、空のファイルに DemoWebClient.aspx という名前を付けて、C:\inetpub\wwwroot\DemoClient に保存しました。

この Web アプリケーションの全体構造を図 7 に示します。

図 7 Web アプリケーション デモ クライアントの構造

<%@ Page Language="C#" Async="true" AutoEventWireup="true"%>
<%@ Import Namespace="System.Threading.Tasks" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Net.Sockets" %>
<%@ Import Namespace="System.IO" %>
<script runat="server" language="C#">
  private static async Task<string> SendRequest(string server,
  private async void Button1_Click(object sender, System.EventArgs e) { . . }
</script>
<head>
  <title>Demo</title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
  <p>Enter service method:
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></p>
  <p>Enter data:
    <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></p>
  <p><asp:Button Text="Send Request" id="Button1"
    runat="server" OnClick="Button1_Click"> </asp:Button> </p>
  <p>Response:
    <asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></p>
  <p>Dummy responsive control:
    <asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></p>
  </div>
  </form>
</body>
</html>

ページの先頭で、関連する .NET 名前空間をスコープに含めるための Import ステートメントと Async=true 属性を含む Page ディレクティブを追加します。

C# スクリプトのコード ブロックには、SendRequest と Button1_Click という 2 つのメソッドを含めます。アプリケーション ページの本文には、入力用の 2 つの TextBox コントロールと 1 つの Button コントロール、サービス応答を保持する出力用の TextBox コントロール、およびアプリケーションが要求に対するサービスからの応答を待機する間に UI の応答性をデモするダミーの未使用 TextBox コントロールを用意します。

Web アプリケーションの SendRequest メソッドのコードは、WinForm アプリケーションの SendRequest のコードとまったく同じです。Web アプリケーションの Button1_Click ハンドラーのコードは、異なる UI に対応するため WinForm の button1_Click ハンドラーとは若干異なっています。

try {
  string server = "mymachine.network.microsoft.com";
  int port = 50000;
  string method = TextBox1.Text;
  string data = TextBox2.Text;
  string sResponse = await SendRequest(server, port, method, data);
  double dResponse = double.Parse(sResponse);
  TextBox3.Text = dResponse.ToString("F2");
}
catch (Exception ex) {
  TextBox3.Text = ex.Message;
}

 

Web アプリケーションのコードも基本的には WinForm アプリケーションのコードと同じですが、呼び出しのメカニズムはかなり異なります。ユーザーが WinForm を使用して要求を行うと、WinForm からサービスに直接呼び出しが行われ、サービスから WinForm に直接応答が返されます。ユーザーが Web アプリケーションから要求を行うと、Web アプリケーションがそのアプリケーションをホストする Web サーバーに要求情報を送信します。その後 Web サーバーからサービスが呼び出され、サービスから Web サーバーに応答が返されます。Web サーバーはその応答を含む応答ページを構築して応答ページをクライアント ブラウザーに送り返します。

まとめ

では、WCF の代わりに非同期 TCP ソケットの使用を考えるのはどのようなときでしょう。約 10 年前、WCF やその前身テクノロジーである ASP.NET Web サービスが作り出される以前は、クライアント サーバー システムを作成する際に最も論理的な選択肢はソケットを使用することでした。WCF の登場は大きな進歩でしたが、WCF で処理するようにデザインされるシナリオが膨大になるため、シンプルなクライアント サーバー システムで使用すると場合によっては面倒な作業になる可能性があります。最新バージョンの WCF は以前のバージョンより構成が容易になったとはいえ、依然として WCF は扱いが難しいことがあります。

個人的には、クライアントとサーバーが別のネットワークに存在する場合やセキュリティを主な留意事項にする場合は常に WCF を使用します。しかし、クライアントとサーバーが 1 つの安全な企業ネットワークに存在するクライアント サーバー システムもたくさんあり、その場合は TCP ソケットを使用することが多くなります。

クライアント サーバー システムを実装する比較的新しいアプローチとして、HTTP ベースのサービス用の ASP.NET Web API フレームワークと、非同期メソッド用の ASP.NET SignalR ライブラリと組み合わせて使用する方法があります。このアプローチは、多くの場面で WCF を使用したアプローチより実装が簡単になり、ソケットベース アプローチよりも低レベルの細かい設定が少なくなります。

Dr. James McCaffrey は、ワシントン州レドモンドにある Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。連絡先は jammc@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Piali Choudhury (MS Research)、Stephen Cleary (コンサルタント)、Adam Eversole (MS Research)、Lynn Powers (MS Research)、および Stephen Toub (マイクロソフト) に心より感謝いたします。