次の方法で共有


Winsock

.NET での高性能なソケットの使用方法を理解する

Daryn Kiely
この記事の一部は、.NET Framework 2.0 のプレリリース版に基づいています。該当部分の情報は変更される可能性があります。

この記事で取り上げる話題:

  • ソケット プログラミングの基本
  • スケーラブルなソケットベース サーバーの作成要素
  • System.Net.Sockets の名前空間
  • ソケットベース アプリケーションのデバッグ方法
この記事で使用する技術:
C#、.NET Framework

サンプルコードのダウンロード:
HighPerformanceSockets.exe (英語) (英語)

(169KB)

目次

ソケット入門
接続方法
サーバー概要
スレッデッド サーバー
多重 I/O への Select の使用
非同期 I/O
スケーラビリティについて
ソケット クライアント アプリケーション
次は何でしょう
ソケット アプリケーションのデバッグ


ソケットは、高性能サーバー アプリケーションのトランスポート手段として、非常に頻繁に利用されています。さいわいにも、Win32 Windows ソケット ライブラリ (Winsock) は、ソケットを使用するプログラムの性能を向上するメカニズムを提供し、Microsoft .NET Framework は、管理下にあるアプリケーションがソケットを利用して通信できるように、Winsock 上にレイヤを提供します (図 1 を参照してください)。最新のソケット サポートは多数が存在し、素晴らしいことですが、このレイヤをすべて自由に使いこなして、本当に高性能なソケットベース アプリケーションを書くには、背景情報が少し必要です。

基本 System.Net.Sockets.Socket クラスを使用して、ソケット ベースのサーバー/クライアントを作成する方法を調査するために、ごく普通のチャット サーバー アプリケーションを書いてみましょう。.NETは、TcpListener クラスや TcpClient クラス等の高レベルな抽象クラスを提供してはいますが (System.Net.Sockets にもあります)、このクラスには、低レベルの Socket クラスによって公開された新しい機能のうち、いくつかは存在しません。それでも、非常に多くの状況で重宝します。TcpListener クラスは、同期モードでブロッキング中に到着した接続要求を聞き取り、引き受けるための簡単な方法を提供し、TcpClient クラスは、同期ブロッキング モードにおいて、ネットワーク上でのストリーム データの接続や、送受信を行うための簡単な方法を提供します。

図 1 Windows ソケット レイヤ

ソケット入門

ソケットは、一般的に、ネットワーク全域に渡る通信のエンド ポイントを定義します。1 つのソケットで、任意の数のプロトコルと対話できます。現在、最も一般的なプロトコルは、User Datagram Protocol (UDP) と Transmission Control Protocol (TCP) です。

UDP ソケットはコネクションレスで、通常は、ブロードキャスト通信およびマルチキャスト通信に利用されています。UDP には、メッセージのトランスポートや順序を保証するための規定がありません。パケットの損失やパケット順序に関する検出と処理は、エンド アプリケーションに一任されます。

TCP ソケットはコネクション指向で、2 つのエンドポイント間に、信頼できる通信路を提供します。TCP の強みは、確実にメッセージを配信できることと、メッセージの順序を適切に処理できる点です。この記事では、TCP ソケットについて説明します。

TCP ソケットは、クライアントとサーバーのどちらにもなります。サーバー ソケットは、接続するクライアントを待ちます。クライアント ソケットは、接続を開始します。ソケット間の接続が確立されると、クライアントとサーバーの双方が、データの送受信や接続を終了できるようになります。

管理しているコード内に TCP サーバー ソケットを設定するには、まず最初に、Socket クラスのインスタンスを作成します。ソケットのコンストラクタが受け付けるパラメータは、AddressFamily、SocketType、ProtocolType の 3 種類です。AddressFamily は、このソケットが使用するアドレッシング スキームを指示します。AddressFamily の値として最も頻繁に使用されている値は、IPV4 アドレス用の InterNetwork と IPV6 アドレス用の InterNetworkV6 の 2 つです。SocketType は、ソケット上で実行する通信のタイプを指示します。最も頻繁に使用されているタイプは、Stream (コネクション指向ソケット向け) と Dgram (コネクションレス ソケット向け) の 2 種類です。ProtocolType は、ソケットが使用するプロトコルを指示するもので、使用できる値は、Tcp、Udp、Idp、Ggp、他の PC のホストなどです。たとえば、TCP 通信用ソケットを作成する場合、次のようにして Socket をインスタンス化します。

Socket s = new Socket(
     AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

ソケットは作成後すぐに、アドレスと結合できます。結合は、クライアント ソケットにはオプションですが、サーバー ソケットには必須です。ソケットとアドレスを結合するには、Socket の Bind メソッドを呼び出します。Bind は、ソケットに関連付けるアドレスとポートを知る必要があるため、Bind メソッドは、パラメータを EndPoint から派生するクラスのインスタンスとして受け入れます。たいてい、これは IPEndPoint クラスのインスタンスになります (これ以外に Framework に含まれている EndPoint 派生クラスは、赤外線ポート通信に使用される IrDAEndPoint だけです)。

IPEndPoint は、ネットワーク通信用エンドポイントの論理表現で、IPアドレスとポートで構成されます。IPEndPoint のコンストラクタは、IP アドレスとポート番号を受け取ります。ポート番号は、単純に使用するポートを表す整数、IP アドレスは .NET IPAddress クラスまたは長整数で表現されます (IP アドレスの各セグメントは、この整数の中にあるバイトの 1 つになります)。IPAddress クラスの静的プロパティから利用できる定義済みの IPAddress がいくつか存在します。TCP ベースのソケットに最も役立つアドレスは、PAddress.Loopback and IPAddress.Any の 2 つです。

IPAddress.Loopback のアドレスは、非常に有名な 127.0.0.1 で、現在のマシンに対してローカルを意味します。これは、ハードウェアやネットワークの接続性にまったく頼らない擬似ネットワーク アドレスです。このため、実際のネットワークやネットワークに使用されるハードウェアを気にすることなく、ローカルにテストを実行できます。このアドレスは、URL のホスト名を "localhost" にする場合に使用するアドレスと同じです。ただし、%windir%\system32\drivers\etc にある hosts ファイルを編集することで、変更もできます。

IPAddress.Any (0.0.0.0) は、サーバーに対して、特定の IP アドレスに関連付けられているインターフェイスだけでなく、ネットワークに存在するすべてのインターフェイス上で行われるクライアント通信を聴取するように指示します (この点については、UDP の動作は、TCP とは少し違います)。なぜ、こちらを常時使用しないのでしょうか。まず、サーバーに接続する人を管理できることが必要になります。1 つのサーバー内で、複数のネットワーク インターフェイス カード (NIC) が使用される場合 (たとえば、社内LAN)、この状況はデュアルホーム サーバーとなりますから、監視するネットワーク アドレスを指定することによって、複数インターフェイス中の 1 つについてのみ、接続を監視すれば良く、他を気にする必要はありません (たとえば、インターネットに接続している NIC)。

第 2 の理由はセキュリティです。ソケットに、あるアドレスを再利用させるように設定する場合で (つまり、既に使用中のアドレスにソケットを結合できます)、このサーバー アプリケーションが使用しているのと同じポートに他の誰かが接続する場合、根底にあるソケットのサブシステムは、そのポートに到着するパケットを、どの結合ソケットで受信するのかを決定しなければなりません。通常は、そのポートへの結合度が (Bind 呼び出しで、より明確にアドレスが指定されて)、他よりも強いサーバー アプリケーションがデータを受け取ります。System.Net.NetworkInformation 名前空間には、システム内に存在するのすべてのネットワーク インターフェイスの詳細な情報を提供するクラスが含まれています。各 NIC のユニキャストな IP アドレスは、次のコードでプリント アウトできます。

foreach (NetworkInterface nic in 
    NetworkInterface.GetAllNetworkInterfaces())
{
    Console.WriteLine(nic.Name);
    foreach (UnicastIPAddressInformation addrInfo in 
        nic.GetIPProperties().UnicastAddresses)
    {
        Console.WriteLine("\t" + addrInfo.Address);
    }
}

特定の NIC に結合するには、対象となる NIC の IP アドレス情報を IPEndPoint より取得し、これを使用します。

接続方法

アプリケーションは、サーバーへの接続を確立するため、または、そのアドレスに結合するために、サーバーに必要なアドレス情報を取得する必要があります。サーバーのアドレスは、System.Net.Dns クラスから取得します。Dns クラスは、転送する IP アドレスを検索するだけでなく、逆にホスト名の検索もします。ネットワーク アドレスと名前解決策の役割を担う静的メンバーで構成されます。Dns.GetHostEntry メソッドは、 マシンのあらゆる関連情報を一覧で持つ IPHostEntry を返します。IPHostEntryは、マシンが DNS サーバーに公開している、すべての IP アドレス一覧を提示します。簡単に説明したいので、この記事では、一覧にある最初のアドレスを使用します。

Bind 呼び出し後、Socket.Listen メソッドは、ソケットのために内部キューを設定します。クライアントが接続を試みる時はいつでも、接続要求はキューに投入されます。Listen メソッドは、引数を 1 つ受け取ります。この引数は、ペンディング状態にできるソケットの最大接続数です。Socket.Accept メソッドは、ペンディングされている先頭の要求をキューから引き抜き、クライアントとの通信に使用される、新しい Socket インスタンスを返します。

クライアント ソケットは、Socket インスタンスを作成後、通常は Connect メソッドを呼び出します。クライアントに特定のポートを使用させる必要がある場合は、先に Bind メソッドを使用します。クライアント ソケットが結合されない場合は、Connect メソッドが自動的にポートを選択します。サーバーへの接続を確立するために、Connect の呼び出しを試みます。Connect は、ターゲットとなるリモート ホストの位置検出に使用する EndPoint を受け取ります。接続できたら、両エンドとも Send メソッドと Receive メソッドを使ってデータ交換できます。

接続して、アプリケーションがこのソケット上での通信を終了したら、ソケットをクローズしなければなりません。Shutdown メソッドを使用すれば、丁寧なクローズを開始できます。つまり、未送信データはすべて送信され、受信されていないデータもすべて受信されます。同時にソケットからデータを読み出している場合は、0 バイトを受け取ったら、ピアがソケットを閉じたことになります。

図 2 に、サーバー ソケットとクライアント ソケットを作成するために必要な手順をまとめます。このように、ソケットの基本は実に単純で、難しいのは、性能の良いソケット アプリケーションを作成することなのです。

Socket メソッドがネットワーク エラーに遭遇すると、SocketException が送出されます。SocketException は、Win32 から逃れたエラー コードを隠ぺいします。SocketException.ErrorCode は、Win32 Winsock で開発している場合に Winsock ライブラリの WSAGetLastError 機能を呼び出したときに受け取るエラー コードと同じです。覚えておきたい Winsock の主要なエラー コードを、図 3 の表に示します。.NET Framework 2.0 では、SocketError で列挙される値から受け取るエラー情報と同じ情報は、SocketException.SocketErrorCode で提供されます。

サーバー概要

サーバー ソケットの初期化では、ソケットを作成し、このソケットをアドレスに結合し、聞き取りキューを準備する必要があります。これから説明するいくつかのサーバーのフォームは、それぞれ異なりながらも、ソケットの初期化に使用しているコードは同じです (図 4 を参照)。

まず、基本的なスレッデッド サーバーを例に、Socket クラスの制限を確認しましょう。基本 API は、ブロッキング呼び出しで構成されるため、スレッド化したサービスを書くときは、接続を引き受け、ソケットの I/O を実行するスレッドを作成する必要があります。

サーバー ソケットを処理するスレッドを、いくつも書かないで済ませる方法が 2 つあります。まず、1 スレッドで複数の I/O ストリームを処理する従来の方法、Select メソッド。もう 1 つは、非同期 I/O を利用する方法です。この 2 つの方法によって、どのようにサーバーを作成するのか、もう少し、詳しく説明します。

スレッデッド サーバー

スレッデッド サーバーを扱うときは、I/O に関わるスレッドを管理して、すべてのソケットに対処することが必要です。初めに検討するのは、Accept 呼び出しです。同期サーバーの場合、Accept 呼び出しを自分のスレッド内で実行することによって、ソケットがタイムリーに受け入れられることを確実にします。ソケット接続の受け入れ処理をする新しいスレッドの生成方法の例を、図 5 に示します。

この処理では、ソケットが受け入れられると、新しいスレッドが、このソケットのための受け入れ処理を発生させます。このスレッドが実行するのは、受け入れたソケットからのデータ受け取りとその処理です (この例では、受け取ったデータを他の接続されているソケットすべてに送信することが、これに相当します)。ある意味、図 5 の例は、基本的要素のみのチャット サーバーです。単純な .NET Framework 2.0 コンソール アプリケーションを作成して、これをテストしてみましょう。Main メソッドに、次のコードを挿入してください。

class Server {
  static void Main(string [] args) {
    ThreadedServer ts = new ThreadedServer(int.Parse(args[0]));
    ts.Start();
    Console.ReadLine();
  }
}

このコードでは、アプリケーションは、結合するサーバー ポートをコマンド行の最初の引数にします。テストするマシンで使用されないポートを指定して、アプリケーションを実行してみましょう。アプリケーション実行中に、コマンド プロンプトをいくつか開き、それぞれ telnet で、テスト サーバーの IP アドレスに接続します。ポートは、サーバー実行時に指定されているポートを使用します。接続が確立されると、1 つのコマンド プロンプト ウィンドウから入力した文字が、直ちに他のすべてのウィンドウにも表示されます。

このスレッド アプローチは、小さいサーバーで多数のクライアントに対応するのには最適で、コードも簡単です。残念ながら、大規模な運用はできません。最大の理由は、作成・破壊されるスレッドの全体数です。筆者のマシンでは、同時接続数を約 1000 件ほど受け入れると、サーバーが "メモリ不足" 例外になります (マシンは、すぐメモリ不足になります。理由は、各実行スレッドが、自身のスタックとして、デフォルトでは 1 メガバイトを確保するためです)。

多重 I/O への Select の使用

各接続ごとにスレッドを作成しないようにする方法は何種類かあります。最初に考えられるのは、単純にスレッドのプールを使用する方法で、先ほどの例で使用したコードへの変更は最低限で済みます。この方法は、同時に存在するスレッドの数を減らす効果があるため、接続が短命であれば実用的です。ただ、チャット サーバーの場合は、サーバへの接続が長期的になることが一般的なので、接続数を制限してプールされるスレッド数を減らす方法では、現実的には利用できない事が多くなります。

これよりも良い解決策は、静的 Socket.Select メソッドを使用する方法です。Select に対して、ソケットについてモニターさせたい項目を指示するリストを 3 つ渡すことができます。項目は、ソケットの信頼性、書き込み権、エラー状況です。Select が戻る時、リストに残っている項目は、各自のリストに指示されている処理への準備を整えます。Socket.Select メソッドは、メソッドがレスポンスを受けるまでの時間を示すパラメータ microSeconds も受け取ります。Select の利用方法の 1 つに、頻繁に解放されるタイマーを作成し、タイマーのイベント ハンドラー内でタイムアウト値をゼロにして Select を呼び出すことで、呼び出しがブロックされないようにする方法があります。ただし、これでは不十分です。ソケットが対応を受けるまでの時間は、タイマーが解放される頻度によりますが、あまり繰り返す回数が多くなるのは良いことではありません。これを防ぐ典型的な方法は、Select を呼び出し、要求を伝える以外には何もしないスレッドを作成することです。

まず、接続を受け付けるときの Select の使い方を見てみましょう (図 6 を参照)。Select 呼び出しのために、リスニング ソケットを読み出しリストに加えます。リスニング ソケットが Select 呼び出し後も読み出しリストに残っている場合は、受け入れを待っている接続が存在しています (Select をこのように使用すると、Accept がブロックされないことを保証する要求かどうかを確認できます)。この時点で、Accept を呼び出し、受け入れたソケットの処理が可能になります。非常に都合の良いことに、受け入れたソケットは、これから到着するデータに対処させるために、Accept ソケットと同じリストに配置する必要があります。これが何を意味するかというと、小さなサーバーで管理するリストは 1 つだけで済むということです。

Select 使用に含まれる他の事項を見てみましょう。Select を呼び出す時、必ず、ソケットについてモニターする項目リストを作成する必要があります。Select が戻る時、このリストは、対応の必要なソケットだけを持つリストに変更されます。これは、一見、良いようですが、実際には不十分です。たとえば、100 個のソケットが同時に I/O 処理を待っている場合を考えてみましょう。100 番目のソケットは、処理されるまで、または、その前にある 99 個のソケットがスケジュールされるまで、ずっと待ち続けなければなりません。さらに、ソケットへの対応は、Select の再エントリを妨げるため、待ちスレッドの窮乏はさらに深刻なものとなります。すべてのソケットに対して I/O 処理を確実にしなければ、次の Select を呼び出すことはできません。これに失敗すると、同じ I/O について、何度も通知を受ける受け取ることになります。

この他にも、Select メソッドのパフォーマンスに関する欠点があります。1000 件程度のクライアントを接続すると、Select の性能が著しく低下することは、明らかです。理由は、カーネルが、利用できるデータがあるかどうかを見極めるために、内部的にすべてのソケットを調査しなければならないためです。

この方法では、1 要求 1 スレッド型に比べて、かなり多くのソケットを接続できますが、これでも、まだ大規模な運用は不可能です。要求に対応するためには、他に 3 つのリストを管理して、各々について、これを繰り返す必要があります。これは、スレッド使用という点から見れば非常に効果的ですが、スレッドベース サーバーを使用する場合に比べると、低い反応です。多発性のソケットを扱うには、もっと良い方法が必要です。

非同期 I/O

サーバーの処理容量を決定する次のステップでは、非同期 I/O を使用します。非同期 I/O は、スレッドの作成と管理に関するニーズを低減します。これにより、コードをさらに単純化できますから、効果的な I/O モデルとなるでしょう。非同期 I/O では、これから到着するデータや接続の処理に、コールバックを利用します。つまり、設定やスキャンを必要とするリストが無いだけでなく、待機中の I/O を処理するための新しいスレッドを作成する必要がありません。

では、.NET Framework は、この非同期呼び出しにどのように対処するのでしょうか。.NET ベースのアプリケーションは、いずれも、各自に関連付けられているスレッド プールを持っています。非同期 I/O 機能が処理待ちのデータを持っている時、.NET のスレッド プールから来るスレッドは、コールバック機能を実行します。コールバックが完了すると、このスレッドは、スレッド プールに戻されます。これは、先に検証したスレッド プールのスレッドが特定の要求に専念する方法とは異なり、プールから来るスレッドは、単独の I/O 処理だけに使用されます。

.NET は、非同期の処理に、非常に単純なモデルを使用します。適切な Begin メソッド (BeginAccept、BeginSend、BeginReceive、等) に、適切なコールバックの代理任命を記述して呼び出すだけです。このとき、結果を得るために、このコールバック呼び出しに、適切な End メソッド (EndAccept、EndSend、EndReceive、等) を含めます。非同期の Begin ルーチンはすべて、ユーザーのあらゆる任意の情報を、コンテクスト オブジェクトとして通過させます。非同期の操作が終了した時、このオブジェクトは、コールバックに渡される IAsyncResult の一部になります。

現在説明している新しい非同期サーバーでは、最初に BeginAccept を呼び出します。この記事で先に説明した 2 つのサーバーは、それぞれ異なる問題を扱っています。スレッデッド サーバーは、それなりの速度がありますが、受け付ける接続数は多くはありません。Select ベースのサーバーは、接続に関しては同じ制限に悩まされませんでしたが、パフォーマンスにおける成功を犠牲にしました。

非同期サーバーの場合、Select ベース モデルに伴う性能のマイナス面の影響を受けることなく、受け付ける接続数の制約を排除できます (いくつかの小さな例外がありますが、例外については、スケーラビリティの節で説明します)。実際、コードがスレッドの作成・破壊に関するオーバーヘッドを負担しないため、性能は、スレッド化するモデルよりも良くなります。

受け入れ処理が完了したら、次に、非同期読み出しをキューに投入します。これにより、サーバーは、スレッドの詳細な調査または作成を行うことなく、ソケットからの読み出しすことができます。非同期の読み出しは、BeingRead への呼び出しで始めます。この結果は、EndRead への呼び出しによって返されます。結果として生じるサーバー (**図 7**を参照) は、Select ベース サーバーに比べて遥かに高性能で、スレッドベース サーバーよりも遥かに多数の接続数を取り扱えます。これは、大規模サーバーとしては、良い解決策ですが、更に性能を向上させることができます。

スケーラビリティについて

筆者のマシンでは、どのサーバー モデルを使用しても、同時接続数は、4000 件を超えません。この時点で、新しいクライアント要求は、バッファ スペース不足、またはキューが多すぎるというエラー メッセージを出して、受け付けられなくなります。このエラー メッセージは、特に役に立つものではありません。この制限は、明らかに、大容量の処理をさせるサーバーには相応しくありません。なぜ、このコードでこのような壁にぶつかるのでしょうか。この制約の原因は、マシンで利用可能なメモリ資源にあります。具体的には、非ページ メモリ プールが原因です。処理が非ページ メモリ プールを消耗すると、それ以上、ソケット接続はできなくなります。

ソケット クライアント アプリケーション

これまでの作業で、非常に反応の高いスケーラブルなサーバーを作成できたので、次に、クライアント アプリケーションを効率的に仕上げましょう。このためのコードは、比較的単純ですが、ソケット クライアントを書く時に、考慮しなければならない点がいくつかあります。

すべてのソケット アプリケーションについて言えることですが、クライアント ソケットは、始めに作成しなければなりません。作成したら、アドレスとポートへの結合は、必ずしも明示的である必要はありません。クライアント ソケットをアドレスに結合しないことに決めた場合は、ネットワーク層が選んだ最適なアドレスが与えられます。ポートを選択していない場合は、ネットワーク層が、1024 から 5000 までの間で一意なポートを提供します。

それでは、なぜ、クライアント ソケットをアドレスやポートに結合するのでしょうか。 マルチホームのクライアント マシンを使用していて、クライアントに NIC を使用させる場合について検証してみましょう。クライアント ソケットを、使用させたい NIC の IP アドレスと結合する必要があります。さいわい、ポート番号 0 で Bind を呼び出すと、この状況でも、ネットワーク層は、1024 から 5000 までの間で一意なポートを提供します。クライアント ソケットを特定のポートに結合することは良い方法ではありませんが、可能ではあります。

ソケットを作成したら、おそらく結合もするかもしれませんが、サーバーに接続してみましょう。多くのサーバー呼び出しと同様に、同期または非同期のサーバー接続が可能です。クライアントが複数のサーバーへの接続を確立する必要がある場合は、非同期 Connect を使用すると有益です。各接続が、前の接続の終了を待つ必要はなく、同時に複数の接続を開始できます。

クライアント ソケットを接続しようとする際に、サーバーが利用できる状況であっても、非常に高い確立で WSAECONNREFUSED エラーを受け取ります。これは、サーバーの受け入れキューが満杯の時に発生します。このため、クライアントは、自身の接続を定期的に再試行することによって、この例外に対処することになるでしょう。

次は何でしょう

非同期ソケット モデルが、最もスケーラビリティの高いものであることを確認しましたが、さらに性能を向上させるには、どうすれば良いでしょうか。サーバーが実行する必要のある機能を検証して、向上させられるポイントを確認しましょう。

サーバーが第 1 に実行しなければならないのは、接続の受け入れです。非同期の要求を受け付ける基本的な方法は 3 種類あり、その内容は、接続を受け入れるだけ、接続を受け入れて、この接続の最初のデータ バイトを受け取る、または、指定されたソケットを使用した接続を受け入れて、この接続の最初のデータ バイトを受け取るというものです。このうち、.NET Framework 1.x で使用できるのは、最初の方法だけです。.NET Framework 2.0 では、この 3 種類すべてを BeginAccept と EndAccept のオーバーロードとして利用できます。

この記事で紹介しているサーバーのコードは、基本的な非同期受け入れに基づいています。接続だけを受け入れる利点は、メッセージ処理ロジックを受け入れロジックから分離できることです。これは、非常に単純なプログラミング モデルですが、これでは、最高の性能を提供することはできないでしょう。

"受け入れて受け取る" 方法の利点は、最初のメッセージに対して性能が上がることです。根本の理由は、負担の多いカーネル呼び出しが少なくて済むことです。この方法は、プロトコルのヘッダーなどの先頭のバイトが少しでも分かっている場合には、非常に上手く動作します。この方法のデメリットは、BeginAccept に提供されるコールバックが、受け取りが完了するまで呼び出されないことです。これにより、接続されているソケットについて、アプリケーションが把握できないという状況が発生する可能性があります。

ソケット内で渡せる "受け入れて受け取る"方法を検証しましょう。この方法は、.NET の世界では、強力な利点となります。特に、.NET Framework 2.0 Socket.Disconnect 呼び出しと組み合わせると非常に強力です。過去の経験では、ソケットの使用を終了したら、接続を閉じて、ソケット オブジェクトを削除しなければなりませんでした。Socket.Disconnect を使用する場合は、Socket インスタンスを再利用できるため、独自の Secket オブジェクト プールを実装する方法が提供されることになります。このような制御をするシナリオでは、長期的に性能を向上できます。

それでは、ソケット接続処理がどのように機能するのかを検証しましょう。まず、OS に接続を聞き取るように教え、指定した数だけの待ち状態の接続を保持しておくためのキューを作成します。最大数を 200 に設定しましょう。200 を超えるクライアントが同時に接続を試みた場合、リスニング キューから溢れたクライアントのそれぞれに、WSAECONNREFUSED エラーを受け取ります (サーバーは、接続を積極的に拒否します)。これを改善するには、複数の受け入れをキュー登録します。集合的受け入れは、受け入れ要求が再送信されなくても、待ち状態の接続をキューから引き出せます。この方法は、リスニング キューの深さを効率的に増大します。**図 7**のコードでは、受け入れる非同期呼び出しを 10 から始めています。

しかし、複数の非同期処理をキューに入れると、負担が大きくなります。非常にスケーラブルなサーバーで行われるような接続を可能にするために、接続数を制限することについては、既に説明しました。こうした非同期呼び出しは、接続の問題を引き起こしていたのと同じリソース プールを枯渇させます。この場合は、スケーラビリティを犠牲にして、優れたパフォーマンスを得ます。

ソケット アプリケーションのデバッグ

.NET Framework 2.0 に投入された新機能の 1 つに、強力なネットワーク追跡能力があります。この機能により、ソケット アプリケーションのデバッグが、デバッガーとパケット探知機だけに頼るのが常だった以前に比べて、ずっと楽にできるようになりました。構成ファイルの中にいくつかのパラメータを設定するだけで、致命的なイベント、エラー、警告、メソッドのエントリと生存、実際のネットワーク トラフィックを見ることができます。これらは、すべて、.NETの標準 Trace 機能によって公表されます。

構成ファイルの System.Diagnostics セクションに、ログを作成したい System.Net 名前空間用のトレース ソースを追加する必要があります (有効なソースは、"System.Net"、"System.Net.Sockets"、"System.Net.Cache" などがあります)。ここでは、System.Net.Sockets を使用します。アプリケーションの構成ファイルのサンプルを図 8 に示します。

ネットワークを追跡できるようになると、ソケット ベース アプリケーションに関する多くの内部情報を見ることができます。System.Net ベースのトレース ログの値の属性はビット マスクです。各ビットは、ログ作成の異なるレベルを表現しています。利用できるレベルは次の 5 つです。ネットワーク トラフィック (0x10)、メソッドのエントリと生存 (0x8)、警告 (0x4)、エラー (0x2)、致命的なイベント (0x1)。図 8 の例では、全項目のログを作成していますから、総合値が 31 です。

メソッドのエントリと生存の追跡は、System.Net 名前空間内で別のメソッドを呼び出す時に、詳細を知るためのツールとして非常に役立ちます。この機能は、自分のプログラムをデバッグできるだけでなく、API の内部的な動きを知ることもできます。戻り値を持つ呼び出しについては、対象となる呼び出しの退出ログ中に戻り値が示されます。以下は、非同期の Accept メソッド呼び出しのエントリと退出の例です。このログから、ソケットがAPI内で作成されたことが分かる点に着目してください。BeingAccept からの戻り値は AcceptOverlappedAsyncResult、この呼び出しのオブジェクト ID は 48209832 です。

System.Net.Sockets Information: 0 : Socket#48285313::BeginAccept()
System.Net.Sockets Information: 0 : Socket#59817589::Socket(InterNetwork#2)
System.Net.Sockets Information: 0 : Exiting Socket#59817589::Socket() 
System.Net.Sockets Information: 0 : Exiting Socket#48285313::BeginAccept()
-> AcceptOverlappedAsyncResult#48209832

すべてのメソッドがネットワーク トレースのフックを提供している訳ではないことを覚えておきましょう。たとえば、Select はトレース ログには現れませんが、Bind は現れます。トレース オプションをサポートしているメソッドを、資料で確認しておきましょう (Network Tracing (英語) を参照)。

ネットワーク トラフィック ログも、非常に有益なツールです。このログは、とても率直で、単純に送信または受信のデータ バッファのダンプです。注意したいのは、バッファは、全体が (指定した最大サイズまで) ダンプされますから、読みにくいでしょう。512 バイト受信バッファの 1 バイトだけを受け取ることもできますが、バッファ全体の内容を見ることになります。自分が確認しているバイトが適切なものであることを確実にするために、送信または受信の戻り値を確認する必要があります。次のトレース ログは、60504909 番のソケットで 1 バイトのデータ ("a") を送信したアプリケーションのログです。

System.Net.Sockets Information: 0 : Socket#60504909::Send()
System.Net.Sockets Verbose: 0 : 00000000 : 61 : a
System.Net.Sockets Information: 0 : Exiting Socket#60504909::Send()
-> 1#1

ログの他の 3 つレベル (警告、エラー、致命的なイベント) では、API 内で起こった失敗の詳細が提供されます。このログ タイプはそれぞれ、ログの先頭に各自のレベルとメッセージに関する詳細があります。たとえば、Socket.GetSocketOptions の呼び出しが失敗した場合、エラーが生成され、次のようなログとして示されます。

System.Net.Sockets Error: 0 : Exception in the 
Socket#48285313::GetSocketOption - An unknown, invalid, or unsupported 
option or level was specified in a getsockopt or setsockopt call

System.Net の新しいログ能力は、非常に強力で、ソケット アプリケーションのデバッグにとても役立ちます。このデバッグ機能を使用する時に気をつけなければならないことは、潜在的に生成できるデータの全体量だけです。デバッグ情報で、すぐにディスクを一杯にしてしまいます。

ご存知のように、ソケット アプリケーションの性能を向上させる方法は、多数あります。この記事では、ソケット API 全体の表面の一部だけを説明しています。


Daryn Kiely は、ネバダ州ラスベガスを拠点とする International Game Technology 社の R & D 部門で活躍中です。連絡先は kiely@lvcm.com (英語) です。


 この記事は、 MSDN マガジン - 2005 年 8 月号からの翻訳です。 .