C#、VB2005 でSocket通信 (複数クライアント&非同期処理) |
|
C#、VB2005のSocket通信サーバーの複数クライアントの非同期処理
前の章では単一クライアント方のSocket通信のサンプルを作成したが、今回は複数クライアントの
サーバーのサンプルを作成します。 かなり複雑になることを予想したが、クラスを使えばかなり簡単に出来る事が判りました。 ただしコーディングより説明の方が難しく、これを読んで判らない人は解らない箇所を明記して 掲示板 で質問して下さい。 掲示板への感想も歓迎します。
仕様
さて仕様ですが、下の画像を御覧頂きたい。
今回はサーバーのみです、クライアントのソフトは C#、VB2005 でSocket通信 を御覧下さい。 今回のサーバーは複数接続ということで、接続のクライアントのハンドルを表示するように ListBoxを設けました。 クライアントが接続してきたら、そのSccketのハンドをListBoxの加えクライアントが 切断したらListBoxからハンドルから削除する様になっています。 又クライアントにメッセージを送る時はListBoxからハンドルを選択して送ることにします。 受信ボックスに送信したクライアントのSocketnoを書いてその下に送信文字を表示します。
コードの構成
前回と同様に送受信にはSystem.Net.Sockets.Socketクラスを使用します。 接続要求の受付はSocketクラスのTcpListnerを使用しますが、このTcpListnerの 要求待ちイベントの Listener.AcceptSocket()は同期メソドで、接続要求が来るまで、 値を返しません、すなわちこのイベントをメインスレッドで行なうと、スレッドが固まってしまいます。 それを避けるために、サーバースタートボタンを押すと、メインスレッドとは別のスレッドを作成して、 その別スレッド上でListener.AcceptSocket()を実行しています。 Listener.AcceptSocket()は接続を許可すると、接続要求をして来たクライアント毎にハンドルを含む Socketクラスのインスタンスを返します。 クライアントとの受送信はこのインスタンスを介して行います。 しかしながら複数接続の場合、接続要求と切断がランダムに発生します、複数のインスタンス の中でどの様に送受信を管理すれば良いのでしょうか?
ClientHandlerクラス
ここではSocketクラスのインスタンスを保持するClientHandlerと言うクラスを作成して、
接続毎にClientHandlerクラスに(実際はClientHandlerクラスのインスタンスに)Socketクラスの インスタンスを渡して保持させます。 ClientHandlerはこのSocketのインスタンスを使用して、個別のクライアントに対する受送信を 行います。 従ってClientHandlerのインスタンスは接続しているクライアントの数だけ作成されます。 もしどれかのクライアントが接続を切断すると、それに対応しているClientHandlerのインスタンスは 捨てられます。 又サーバー側から何かのデーターをクライアントに送りたい場合も、このClientHandlerのインスタンスが 使用されます。 複数接続され作成されたClientHandlerを識別する為にClientHandlerに渡したScketのプロパティである Handleを使用します。 ただしこれは簡易的なものでその時存在するSocketのインスタンスのHandleは重複はしませんが、 一度接続を切り再接続して出来るSocketのHandleが以前のものと同じでない保証は有りません。 あくまでも現在接続しているクライアントを識別する為であると思ってください。
受信処理
ClientHandlerクラスの中には非同期で送られて来たデータを読み込むメソドと、非同期でデータを
書き込むメソドが含まれています。(非同期に関しては後で説明します) データ受信の場合は各クライアントに対応したClientHandlerのインスタンスの中で受信が行なわれ 受信終了時はそのClientHandlerのインスタンスの中のメソドが呼ばれます、従ってそのメソドの中から メインフォームのTextBoxにSocketのインスタンスのハンドルと受信文字を書き込んでいます。
送信処理
送信の場合はClientHandlerのインスタンスの中から送信したいクライアントのインスタンスを探し、
その中の送信メソドを呼ぶ事になります。 実際はClientHandleの中でNetworkStreamのインスタンスを作成してそインスタンスのBeginWriteメソドを 使用して送信を行います。 この操作は現在存在しているClientHandlerのインスタンスを列挙してその中から目的のクライアントの ハンドルを持つインスタンスを探してそのインスタンスの送信メソドを呼ぶ事になります。 その方法として直ぐに思い浮かぶ方法はClientHandlerのインスタンスの配列を作って管理する方法です。 クライアントが接続した時に作られたClientHandlerのインスタンスを配列に加え、切断した時には 配列から削除すればよい訳です。 勿論これでも構いません、しかしFrameWork上ではもっと良い方法が有ります、List 方法です。 FrameWork上では配列は固定です、配列の大きさが変化する場合は特殊な操作が必要となります。 Listクラスは任意のオブジェクトを保持できるばかりか削除や追加も簡単に行えます。 本プログラムではClientHandlerのインスタンスをListクラスに保持させています。 すなわち新しいクライアントが接続しClientHandlerのインスタンスが作成されたら そのインスタンスをLsitクラスに加え、切断されたらListから削除します。 さて本題の送信の場合はListクラスの中から目的のHandleを持つClientHandlerのインスタンスを探し そのインスタンスの送信メソドを呼び出せば良いことになります。 ListにClientHandlerのインスタンスを加えたり、ListからClientHandlerのインスタンスを 削除するのはメインスレッド以外のスレッドからデリゲートを使用してメインスレッド 上で行いますので、デリゲートの使用が必要となります。 下のコードはそのデリゲートの宣言です。 C#のコード
//別スレッドからClientHandlerを持つList VB2005のコード
'別スレッドからClientHandlerを持つList
別スレッドからの操作
ClientHandlerのコンストラクタは2つの引数を持ちます。
1つ目は既に説明したClientHandlerクラスのインスタンスです。 2つ目はメインフォームのインスタンスへの参照です。 ClientHandlerの中の送受信処理は全てメインフォームとは別のスレッドで行われるます。 従ってメインフォーム上のTextBoxnなどに直接受信文字などを書き込むことは出来ません。 FrameWorkのControlクラスには、別スレッドからコントロールを操作する場合に使用出来る Invokeメソッドが用意されています。 FormをはじめTextBoxは全てこのControlクラスから派生しているので、このInvokeメソドを持っています。 Invokeメソッドを使うと、コントロールに対する操作をメイン・スレッドで実行させることが出来るのです。 実際にはメインスレッド上で実行したいメソッドに対応したデリゲートを作成し、そのデリゲートの インスタンスをInvokeメソッドのパラメータで指定して呼び出します。 ClientHandlerの2番目の引数にメインフォームのインスタンスを渡している理由は、 メインフォームが持つInvokeメソドを使用する為です。 メインフォーム上のコントロール(TextBox等)もInvokeメソドを持っていますので、これらを引数に 指定しても構いません。
同期処理と非同期処理
同期処理とは現在進行中の処理が終了するまで、次の処理を行わない方法です。 非同期処理は現在の処理とは無関係に他の処理が行われる方法です。 上で説明した Listener.AcceptSocket()は同期メソドです、この為Listener.AcceptSocket()を メインスレッドで行うことは出来ません、 Listener.AcceptSocket()が終了するまですなわち クライアントが接続要求を出すまで画面が固まってしまいます。 それを避けるためにListener.AcceptSocket()は別スレッド上で呼び出されます。 マルチスレッドの場合何も指定しなければスレッド間は非同期になります。
非同期I/O
FrameWorkには非同期I/Oなるものが有ります、これは時間のかかるファイルやネットワークからの
読み込みや書き込みを非同期に行うものです。 非同期I/OではstreamメソドのBeginReadとBeginWriteを使用します。 System.Net.Sockets.Streamクラスもこれと同じに、BeginReadとBeginWriteメソドを持ちます。 BeginReadは次の5つの引数を持ちます。 1、読み込みの結果を格納するバッファ 2、読み込みの開始点 3、バッファのサイズ 5、コールバック用のデリゲート 6、状態を取得する為のオブジェクト BeginReadが呼び出されると別スレッドを作成してネットワークからの受信を始めます。 受信が終了すると、5番目の引数に指定したデリゲートを使用してコールバックと呼ばれる 通知を登録されたメソドOnReadCompleteに行います。 OnReadCompleteは private void OnReadComplete(IAsyncResult ar) Private Sub OnReadComplete(ByVal ar As IAsyncResult) の様にIAsyncResultの引数を持っていて int bytesRead = networkStream.EndRead(ar); Dim bytesRead As Integer = networkStream.EndRead(ar) の様に読みと取りのバイト数などが取得できます。 更にクライアントがSocketの切断を行った場合もOnReadCompleteに通知が入ります。
動作確認
さて上記プログラムの動作確認ですが、複数のPCは必要有りません。一台のPCでサーバーと複数のクライアントの接続実験が可能です。 先ず下のソースコードをダウンロードしてください。 これを解凍の上「Asyncc.csproj」をVS2005で立ち上げ「ビルド」します。 そして出来上がる「bin」フォルダの中の「Debug」又は「Releas」の中の「SocketAsyncC.exe」を 実行します。これでサーバーが立ち上がります。 クライアントの方は 「C#、VB2005 でSocket通信」 の下に有るコードをダウンロード解凍してビルドします。 そしてやはりWinSockC.exeを実行します、こちらはクライアントですから複数立ち上げます。 クライアントの方はIPアドレスを入力します。 この場合は同じコンピューターでサーバーとクライアントを動作させますから、自分のPCのIPアドレス を入れる事になります。 この状態でそれぞれ「Start」を押すと接続が開始されます。 送信ボックスに何らかのメッセージを入れて送信ボタンを押すとサーバーとクライアント双方に 送信されます。 サーバーからクライアントへの送信は、クライアントの選択を忘れずに行って下さい。 以下が実行画面となります。
最後に
ClientHandlerクラスに関してはコードは「オライリー・ジャパンの「プログラミング C#」を参考にしました。 この本をお持ちの方の為に極力関数名などを同じにして有るので見比べていただきたいと思います。 この本とここに上げたプログラムとの違いは、「プログラミング C#」ではプログラムはコンソールプログラムで サーバーはクライアントからメッセージを受け取るとそのままそのメッセージを返す仕様になっていますが、 これらをWindowスタイルに変え、接続されているクライアントのインスタンスをLISTクラスに入れて、 接続している任意のクライアントに任意のメッセージを送る事が可能な様に変更しています。 ブログ NetworkStream.BeginRead も合わせてお読みください。 |