- PR -

非同期ソケットでの受信タイムアウト設定方法について

投稿者投稿内容
しろぱんだ
会議室デビュー日: 2006/03/28
投稿数: 11
投稿日時: 2006-04-11 17:09
お世話になっております、しろぱんだと申します。

OS:Windows XP Professional(SP2)
環境:VS2003(.Net Framework 1.1)
言語:C#

現在、ソケット通信のサーバアプリケーション(とクライアントアプリケーション)を作成しています。

『非同期サーバソケットの例』
http://msdn.microsoft.com/library/ja/default.asp?url=/library/ja/cpguide/html/cpconnon-blockingserversocketexample.asp

上記記事やその他諸々を参考に、非同期サーバソケットによる送受信を実現しようとしているのですが、受信タイムアウトの設定を行うために、SetSocketOptionメソッドを使用してReceiveTimeoutの値をセットした(※1)のですが、なぜか指定秒数が経ってもタイムアウトエラーが発生しません。
尚、クライアント側では
・サーバに接続
・Thread.Sleep(10000)で10秒間待つ
・メッセージの終端文字(仮に";"としています)を付けずにSendメソッドでメッセージを送信
・ソケットをシャットダウン+クローズ
という流れで処理をしています。
サーバ側のBeginAcceptコールバック、BeginReceiveコールバックの部分は以下の通りです。

//##### ここから ######

/// <summary>
/// BeginAcceptのCallback関数
/// </summary>
private void AcceptCallback(IAsyncResult ar)
{
 Socket client = null;
 try{
  lock(this.syncObject){ client = this.listener.EndAccept(ar); }
  // ReceiveTimeoutを3秒に設定(※1)
  client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 3000);
  StateObject so = new StateObject(client);
  // データ受信開始
  client.BeginReceive(so.buffer,0,StateObject.BufferSize,0,new AsyncCallback(ReceiveCallback),so);
 } catch (Exception ex) {
  Console.WriteLine(ex.Message + "\r\n" + ex.StackTrace);
  lock(this.syncObject){ this.CloseClient(client); }
 } finally {
 }
 // 接続要求受け入れ再開
 this.listener.BeginAccept(new AsyncCallback(this.AcceptCallback), null);
}

/// <summary>
/// BeginReceiveのCallback関数
/// </summary>
private void ReceiveCallback(IAsyncResult ar)
{
 Socket client = null;
 StateObject so = null;
 int len = -1;
 string sendStr = "";
 string content = String.Empty;
 try
 {
  lock(this.syncObject)
  {
   so = (StateObject)ar.AsyncState;
   client = so.worker;
   len = client.EndReceive(ar);
  }
  // 切断の検知
  if(len < 1)
  {
   throw new ApplicationException(string.Format("クライアント({0}) のデータ受信中に切断されました。",so.workerIP));
  }
  // 受信データ蓄積
  so.sb.Append(Encoding.Unicode.GetString(Encoding.Convert(Encoding.GetEncoding("Shift_JIS"),Encoding.Unicode,so.buffer,0,len)));
  content = so.sb.ToString();
  // メッセージの終端文字を検知
  if(content.IndexOf(";") > -1)
  {
   Console.WriteLine(string.Format("クライアント({0}) から受信しました。" + "\r\n" + content,so.workerIP));
   sendStr = content;
   this.Send(so, sendStr);
   // 切断
   lock(this.syncObject){ this.CloseClient(client); }
  } else {
   // 末尾まで到達していない場合は、受信再開
   lock(this.syncObject)
   {
    client.BeginReceive(so.buffer,0,StateObject.BufferSize,0,new AsyncCallback(ReceiveCallback),so);
   }
  }
 } catch(Exception ex) {
  if(client != null)
  {
   Console.WriteLine(string.Format("クライアント({0}) のデータ受信中にエラーが発生しました。" + "\r\n" + ex.Message + "\r\n" + ex.StackTrace,so.workerIP));
   // 切断
   lock(this.syncObject){ this.CloseClient(client); }
  }
  else
  {
   Console.WriteLine(ex.Message + "\r\n" + ex.StackTrace);
  }
 }
 finally
 {
 }
}

//##### ここまで #####

現状は、希望するタイムアウトエラーは発生せず、クライアントの切断を検知して終了になってしまいます。
BeginReceiveメソッドを使用した場合、ReceiveTimeoutは効かないものなのでしょうか。
(そんな訳はないような気がしないでもないのですが、あるような気がしないでもなく。。。)
非同期サーバソケットにて受信タイムアウトを設定する方法をアドバイス頂ければと思います。
なにとぞ、宜しくお願いします。
しろぱんだ
会議室デビュー日: 2006/03/28
投稿数: 11
投稿日時: 2006-04-12 09:45
自己解決といいますか、

『SocketOptionName 列挙体』
http://msdn2.microsoft.com/ja-JP/library/system.net.sockets.socketoptionname.aspx

上記記事のReceiveTimeoutの箇所で
「非同期メソッドに対しては機能しません」
の一文を見つけました。
読み込みが足りないまま投稿として投げてしまって申し訳ありません。

ReceiveTimeoutを使ったタイムアウトの設定ができないことは分かりました。
その上で別途伺いたいのですが、非同期メソッドを使用する場合、タイムアウトを実装したければどのような方法で実現するものなのでしょうか。

一つ考え付いたのは、BeginReceiveの前にスレッドを一つ起動して、そちらである時間間隔毎に進行状況をチェックしておいてタイムアウトになったら該当するソケットのEndReceiveを実行してシャットダウンする。
といった処理ならできるのかもしれない、とおぼろげに考えたのですが、そうすると無駄にスレッドを乱立させてしまうので余り良くない処理だと思うのです。

よろしければ、アドバイスやヒントなどを頂けると有難いです。
しろぱんだ
会議室デビュー日: 2006/03/28
投稿数: 11
投稿日時: 2006-04-12 15:46
なんとなく自己解決しそうです。

基本的な非同期サーバソケットの動作はそのままに、

1)Socketを継承した、またはメンバに持つクラス(S)を自作しておく。(※1)
2)サーバの開始時に、受け入れたクライアントに対応する(S)を格納するコレクション(C)を生成しておく。(※2)
3)サーバの開始時に、クライアントのタイムアウトチェック用のスレッド(T)を開始しておく。
4)クライアントの接続要求が来たら、そのソケットの情報を渡した(S)を生成して(C)に追加しておく。
5)(T)で、定期的に(C)に格納されている(S)がタイムアウトになっているかどうかをチェックして、タイムアウトが発生している(S)が存在した場合は必要な処理をする。

(※1)
接続開始の時間や、受信開始の時間を保持するように作っておく。
(※2)
System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList())

といった処理をすれば、タイムアウトは処理できるような気がしてきました。
(検証はまだしていません。申し訳ありません。。。)

考え方が間違っていたり、よくない処理方法である場合には、ご指摘頂けると助かります。
しろぱんだ
会議室デビュー日: 2006/03/28
投稿数: 11
投稿日時: 2006-04-13 22:22
まだ検証は完全には済んでいないのですが、取り敢えず前述の方法で何とかなりそうです。

……が、スタンダードな手法からは遠い方法をとっているような気がしてなりません。(疑心暗鬼?)
「おおよそ合っている」
「間違っている」
「書いてる意味が良く分からない」
「過去ログを見るように」etc...
何か一言頂けると、とても有難いです。
末記人
大ベテラン
会議室デビュー日: 2005/12/05
投稿数: 233
お住まい・勤務地: あわにこ
投稿日時: 2006-04-13 22:34
こんばんは

別スレッドで同期ソケットを使用するのはダメですか?

※別スレッドの同期ソケットはメインスレッド(呼び出し側)から見たら非同期ソケット
として扱えるんじゃないでしょうか?
非同期=ノンブロッキングという意味ならですが...

コールバックなどは自力での実装になるとおもいますが...


[ メッセージ編集済み 編集者: 未記入 編集日時 2006-04-13 22:39 ]
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-04-13 22:38

スタンダードかどうかは分かりませんが、私なら Socket の非同期機構を使わずに、接続クラスを作って自前で Thread を起動させることで非同期通信を行います。
クライアントからの接続を受け入れるごとに接続クラスを作成し、接続クラスは独自にスレッドを作成してクライアントと送受信を行います。タイムアウトはタイマを接続クラスのメンバに置くなどして使用します。
//あ、これなら ReceiveTimeout も使えるか。
接続クラスを管理するクラスを用意しておけば、接続のキューイングなども対応できます。
しろぱんだ
会議室デビュー日: 2006/03/28
投稿数: 11
投稿日時: 2006-04-14 18:28
お二人とも返答ありがとうございます。

各クライアント毎に別スレッドを起動して処理させる方法については、スレッドの生成・破棄にリソースが多くかかるという記述を見かけたので、同時接続数が少ない場合を除いてはあまり選択しない方が良いのではないかと思っていました。
現在想定しているのが、同時接続数が数十〜百未満程度で、クライアント−サーバ間の接続はクライアントが任意のタイミングで切断するまでずっと保持するようなアプリケーションのため前述の方法を避けていたのですが、特に意識しなければならないような問題ではないんでしょうか?
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-04-14 19:42
引用:

各クライアント毎に別スレッドを起動して処理させる方法については、スレッドの生成・破棄にリソースが多くかかるという記述を見かけたので、同時接続数が少ない場合を除いてはあまり選択しない方が良いのではないかと思っていました。

現在想定しているのが、同時接続数が数十〜百未満程度で、クライアント−サーバ間の接続はクライアントが任意のタイミングで切断するまでずっと保持するようなアプリケーションのため前述の方法を避けていたのですが、特に意識しなければならないような問題ではないんでしょうか?


様々な要素が影響するものですから一概には言えませんが、数十からのクライアントが同時に接続してきて、かつそれぞれがそれなりに長く接続するというのなら、Socket の非同期接続機構を使った場合、下手するとスレッドプールが枯渇する可能性もあります(CPU 一つにつき 25 スレッドしか並行処理を行いません。これは例えば System.Timers.Timer を処理するスレッドなども含んでの数です)。
自前でスレッドを作成するコストが問題になるのは、HTTP(1.0) のような「大量の接続が要求されるが、一つ一つの接続は短時間」というケースです。例えばブラウザでとあるページを開いても、そのページに含まれる画像など一つ一つに接続が作成されるわけで、一つのクライアントから下手すれば何十とリクエストが生成されます。それが何人ものクライアントから発せられるとなるとそれはもう大変なリクエストの数になりますよね。しかしそれぞれの接続は要求を受けたらファイルを返して切断すれば良いだけですから、一つ一つの接続はごく短い時間で済みます。こういうときは、多すぎる接続は一旦キューに置いて、順次処理していくという構成が成り立ちます。
しかし、一度に処理できる上限を超えた接続は、現在処理している接続が完了するまで待たされるわけで、ある程度一つの接続が長くなるのならこれはいささか問題があります。
とくにスレッドプールを使う場合、こちらで同時処理数を指定できないのがネックになります。
そのくらいの構成なら、自前でスレッドを用意するのも特にそう大きな問題ではないかと思います。

スキルアップ/キャリアアップ(JOB@IT)