none
connectを繰り返すループ処理で、途中でconnectに失敗しないようにするには? RRS feed

  • 質問

  • いつもお世話になっております。
    Windows 7 Enterprise x64 + VS2010 Professional のVC++ を使って、SDKで開発を行っております。

    現在、WinSockプログラムにおいて、

    ・サーバ側はクライアントからの接続を待機(accept関数)し、クライアントからパケットが来たら、データを受信
         ⇒受信が終わったら再び待機
    ・クライアント側は不定期に内部でフラグが上がったら、格納しているデータをサーバに送信


    以上のようなプログラムを作成しています。
    どちらのアプリも当初は問題なく動作しているように見えたのですが、長時間稼働していると、クライアント側でconnect関数に失敗しデータを送信できなくなりました。

    netstat -nなどで調査したところTIME_WAITになっているものが大量にあり、ネットで調査し

    http://www.geekpage.jp/programming/winsock/so_reuseaddr.php

    を参考に、setsockopt関数で、SO_REUSEADDRオプションをつけてみたのですが…状況が変わらないんです。
    connect関数失敗時のエラー内容は、これから取得しようとソースにデバッグ用のコードを追加中なのですが、サーバ側を落としあげすると、また数時間は問題なくパケットのやり取りを行います。

    サーバ側、クライアント側でそれぞれ送信・受信時に行っているスレッド・関数は以下の通りです。
    (エラー処理などは省略しています)

    //サーバ側で受信を待機しているスレッド(引数にスレッド終了イベントのハンドルを渡しています)
    DWORD ReceiveProductNumberProc(void *param)
    {
         SOCKET l_Sock0;
         SOCKET l_Sock_Accept;
         sockaddr_in l_addr;
         sockaddr_in l_client;
         int l_len;
         HANDLE l_hEvents[2] = {NULL, (HANDLE)param}
         DWORD l_dwRet;
         BYTE *l_pBuf = NULL;
         BYTE *l_pBufTop = NULL;
         int l_ReceiveDataSize;                                                                           //受信するデータのサイズ
         int l_ReceivedDataSize;                                                                           //受信したデータのサイズ
         int l_DataRemainderSize;
    //SENDDATA構造体は、送信するデータをひとまとめにしたオリジナルの構造体です。
         SENDDATA l_UpdateData;
         BOOL l_fYes = TRUE;

         l_ReceiveDataSize = sizeof(SENDDATA);
         l_pBuf = (BYTE *)malloc(l_ReceiveDataSize);

         //先頭のアドレスを控えておく
         l_pBufTop = l_pBuf;


         //パケット受信用のハンドルを生成
         l_hEvents[0] = WSACreateEvent();

         //ソケットの準備
         l_Sock0 = socket(AF_INET, SOCK_STREAM, 0);

         //受信準備
         l_addr.sin_family = AF_INET;
         l_addr.sin_port = htons(4463);
         l_addr.sin_addr.S_un.S_addr = INADDR_ANY;

         setsockopt(l_Sock0, SOL_SOCKET, SO_REUSEADDR, (const char *)&l_fYes, sizeof(l_fYes));
         bind(l_Sock0, (sockaddr *)&l_addr, sizeof(l_addr));
         listen(l_Sock0, 5);

         //受信イベントの生成
         WSAEventSelect(l_Sock0, l_hEvents[0], FD_ACCEPT);

         //受信
         while(1)
         {
              l_dwRet = WSAWaitForMultipleEvents(2, l_hEvents, FALSE, WSA_INFINITE, FALSE);
              if(l_dwRet - WSA_WAIT_EVENT_0 == 0)
              {
                   //シグナルになったらAccept
                   l_len = sizeof(l_client);
                   //パケットの受信を待機する
                   l_Sock_Accept = accept(l_Sock0, (sockaddr *)&l_client, &l_len);
                   if(l_Sock_Accept != INVALID_SOCKET)
                   {
                        //パケットが来たら受信開始(最後まで受信するまでループ)
                        //先頭アドレスに戻る
                        l_pBuf = l_pBufTop;
                        ZeroMemory(l_pBuf, sizeof(SENDDATA));
                        l_DataRemainderSize = l_ReceiveDataSize;
                        do
                        {
                             l_ReceivedDataSize = recv(l_Sock_Accept, (char *)l_pBufTop, l_DataRemainderSize, 0);
                             if(l_ReceivedDataSize == SOCKET_ERROR)
                             {
                                  //エラーになった場合はループを抜ける
                                  break;
                             }
                             //受信できた場合は受信した分を足して、ポインタも移動する
                             l_pBuf += l_ReceivedDataSize;
                             l_DataRemainderSize -= l_ReceivedDataSize;
                        }while(l_DataRemainderSize > 0);

                        //ここで取得したデータを処理しています


                        //再受信待機のためいったんソケットを閉じる
                        closesocket(l_Sock_Accept);
                   }
                   WSAResetEvent(l_hEvents[0]);
              }
              else if(l_dwRet - WSA_WAIT_EVENT_0 == 1)
              {
                   //停止イベントだったらループを抜ける
                   shutdown(l_Sock_Accept, SD_BOTH);
                   closesocket(l_Sock_Accept);
                   break;
              }

         }

         //スレッド終了時にはソケットを閉じる
         shutdown(l_Sock0, SD_BOTH);
         closesocket(l_Sock0);

         //イベントハンドルが閉じられていなかったら閉じる
         if(l_hEvents[0] != NULL)
         {
              WSACloseEvent(l_hEvents[0]);
              l_hEvents[0] = NULL;
         }

         //バッファの解放
         if(l_pBufTop != NULL)
         {
              free(l_pBufTop);
              l_pBufTop = NULL;
         }
         return 0;
    }


    //クライアント側からサーバへデータを送信する関数
    //PSENDDATAは、サーバ側で利用しているSENDDATA構造体のポインタです。
    BOOL SendResult(PSENDDATA pResultData, BSTR ControlAppIP)
    {
         char *l_mbControlAppIP = NULL;
         SOCKET l_Sock;
         sockaddr_in l_server;
         int l_SentDataSize;
         int l_DataRemainderSize;
         SENDDATA l_SendData;
         PSENDDATA l_pSendData = &l_SendData;
         CopyMemory(&l_SendData, pResultData, sizeof(SENDDATA));

         if(l_pSendData == NULL)
         {
              return FALSE;
         }
         //IPをchar型に変換
         l_mbControlAppIP = _com_util::ConvertBSTRToString(ControlAppIP);
         //ソケットの準備
         l_Sock = socket(AF_INET, SOCK_STREAM, 0);

         l_server.sin_family = AF_INET;
         l_server.sin_port = htons(4463);
         l_server.sin_addr.S_un.S_addr = inet_addr(l_mbControlAppIP);

         //何度もこの関数を実行していると、途中からここで必ず失敗するようになります
         connect(l_Sock, (sockaddr *)&l_server, sizeof(l_server)) != 0)

         //connectできたら接続データ送信
         l_DataRemainderSize = sizeof(SENDDATA);
         do
         {
              l_SentDataSize = send(l_Sock, (const char *)l_pSendData, l_DataRemainderSize, 0);
              l_pSendData += l_SentDataSize;
              l_DataRemainderSize -= l_SentDataSize;
         }while(l_DataRemainderSize > 0);

         shutdown(l_Sock, SD_BOTH);
         closesocket(l_Sock);

         if(l_mbControlAppIP != NULL)
         {
              delete[] l_mbControlAppIP;
              l_mbControlAppIP = NULL;
         }
         return TRUE;
    }


    こんな感じで作成しています。
    サーバ側のsetsockopt関数の位置が悪いなど、何か問題があるのでしょうか?

    ご存知の方いらっしゃいましたら、ご教授を願います。
    よろしくお願いいたします。

    2011年2月15日 3:50

回答

  • 直接の解決策になるかどうか分かりませんが、二点コメントします。

    ・Graceful Shutdown について

     http://msdn.microsoft.com/en-us/library/ms738547(VS.85).aspx

    にある表の、クライアント側での振る舞いとして、「shutdown() の後、切断(FD_CLOSE)を検出するまで recv() を呼び出す。その後、closesocket() を呼ぶ」と書かれていますよね。ご提示のコードでは、クライアント側での、shutdown() と closesocket() の間での recv() の呼び出しがありません。
    その処理を入れて改善するかどうかは分かりませんが、次の二番目のコメントの方策を試す手間が大きい場合は、念のため、一度試してみて下さいませ。


    ・コネクションの開設・切断回数を減らす。
    コネクションを短時間に大量に開設・切断することが要因となっていますので、その回数を減らすことで改善できると思います。
    具体的には、次の改訂を加えるのは、いかがでしょうか:

     - クライアント側の SendResult() 内部で connect() せず、SendResult() の呼び出し側で connect() してソケットを渡すようにする。

     - SendResult() の呼び出し側では、送出するデータが一定時間以上ない場合(※一定時間以上、SendResult() を呼び出す必要がない場合)、サーバとのコネクションを切断する。コネクションを切断していない場合は、SendResult() を呼び出す際、それを再利用する。

    SendResult() を呼び出す側の処理の改訂が若干多いかも知れませんが、これが根本的な解決策ではないかと思います。

    • 回答としてマーク どらちん 2011年2月23日 2:18
    2011年2月18日 3:59
  • l_hCloseEventのハンドルがNULLだからではないでしょうか?

    HANDLE l_hCloseEvent = WSACreateEvent();

     

    • 回答としてマーク どらちん 2011年2月23日 2:17
    2011年2月18日 7:13

すべての返信

  • サーバ側の受信処理が終わった後、closesocket() だけを呼び出していて、"Gracefull Shutdown" が行われていないのが原因かも知れません。"Gracefull Shutdown" の解説と、closesocket() のリファレンスをご覧になってみて下さい:

     http://msdn.microsoft.com/en-us/library/ms738547(VS.85).aspx
     http://msdn.microsoft.com/en-us/library/ms737582(VS.85).aspx

    2011年2月15日 7:27
  • 古賀信哉さん

    レスありがとうございます。
    http://msdn.microsoft.com/en-us/library/ms738547(VS.85).aspx
    こちらの最後にある表、わかりやすいですね。

    ループの中でも、受信が終わったらshutdownをしないといけないようなので、ソースに追加して走らせてみます。
    確認するのに数時間かかるので、走らせて放置プレイしてみます!!

    結果はまた後日、ご報告させていただきます。

    2011年2月15日 8:27
  • shutdownを以下のように追加してみましたがダメでした・・・
    (変更箇所を太字にしてみました)

    //サーバ側で受信を待機しているスレッド(引数にスレッド終了イベントのハンドルを渡しています)
    DWORD ReceiveProductNumberProc(void *param)
    {
         SOCKET l_Sock0;
         SOCKET l_Sock_Accept;
         sockaddr_in l_addr;
         sockaddr_in l_client;
         int l_len;
         HANDLE l_hEvents[2] = {NULL, (HANDLE)param}
         DWORD l_dwRet;
         BYTE *l_pBuf = NULL;
         BYTE *l_pBufTop = NULL;
         int l_ReceiveDataSize;                                                                           //受信するデータのサイズ
         int l_ReceivedDataSize;                                                                           //受信したデータのサイズ
         int l_DataRemainderSize;
    //SENDDATA構造体は、送信するデータをひとまとめにしたオリジナルの構造体です。
         SENDDATA l_UpdateData;
         BOOL l_fYes = TRUE;

         l_ReceiveDataSize = sizeof(SENDDATA);
         l_pBuf = (BYTE *)malloc(l_ReceiveDataSize);

         //先頭のアドレスを控えておく
         l_pBufTop = l_pBuf;


         //パケット受信用のハンドルを生成
         l_hEvents[0] = WSACreateEvent();

         //ソケットの準備
         l_Sock0 = socket(AF_INET, SOCK_STREAM, 0);

         //受信準備
         l_addr.sin_family = AF_INET;
         l_addr.sin_port = htons(4463);
         l_addr.sin_addr.S_un.S_addr = INADDR_ANY;

         setsockopt(l_Sock0, SOL_SOCKET, SO_REUSEADDR, (const char *)&l_fYes, sizeof(l_fYes));
         bind(l_Sock0, (sockaddr *)&l_addr, sizeof(l_addr));
         listen(l_Sock0, 5);

         //受信イベントの生成
         WSAEventSelect(l_Sock0, l_hEvents[0], FD_ACCEPT);

         //受信
         while(1)
         {
              l_dwRet = WSAWaitForMultipleEvents(2, l_hEvents, FALSE, WSA_INFINITE, FALSE);
              if(l_dwRet - WSA_WAIT_EVENT_0 == 0)
              {
                   //シグナルになったらAccept
                   l_len = sizeof(l_client);
                   //パケットの受信を待機する
                   l_Sock_Accept = accept(l_Sock0, (sockaddr *)&l_client, &l_len);
                   if(l_Sock_Accept != INVALID_SOCKET)
                   {
                        //パケットが来たら受信開始(最後まで受信するまでループ)
                        //先頭アドレスに戻る
                        l_pBuf = l_pBufTop;
                        ZeroMemory(l_pBuf, sizeof(SENDDATA));
                        l_DataRemainderSize = l_ReceiveDataSize;
                        do
                        {
                             l_ReceivedDataSize = recv(l_Sock_Accept, (char *)l_pBufTop, l_DataRemainderSize, 0);
                             if(l_ReceivedDataSize == SOCKET_ERROR)
                             {
                                  //エラーになった場合はループを抜ける
                                  break;
                             }
                             //受信できた場合は受信した分を足して、ポインタも移動する
                             l_pBuf += l_ReceivedDataSize;
                             l_DataRemainderSize -= l_ReceivedDataSize;
                        }while(l_DataRemainderSize > 0);

                        //ここで取得したデータを処理しています


                        //再受信待機のためいったんソケットを閉じる
                        shutdown(l_Sock_Accept, SD_BOTH);
                        closesocket(l_Sock_Accept);
                   }
                   WSAResetEvent(l_hEvents[0]);
              }
              else if(l_dwRet - WSA_WAIT_EVENT_0 == 1)
              {
                   //停止イベントだったらループを抜ける
                   shutdown(l_Sock_Accept, SD_BOTH);
                   closesocket(l_Sock_Accept);
                   break;
              }

         }

         //スレッド終了時にはソケットを閉じる
         shutdown(l_Sock0, SD_BOTH);
         closesocket(l_Sock0);

         //イベントハンドルが閉じられていなかったら閉じる
         if(l_hEvents[0] != NULL)
         {
              WSACloseEvent(l_hEvents[0]);
              l_hEvents[0] = NULL;
         }

         //バッファの解放
         if(l_pBufTop != NULL)
         {
              free(l_pBufTop);
              l_pBufTop = NULL;
         }
         return 0;
    }


    //クライアント側からサーバへデータを送信する関数
    //PSENDDATAは、サーバ側で利用しているSENDDATA構造体のポインタです。
    BOOL SendResult(PSENDDATA pResultData, BSTR ControlAppIP)
    {
         char *l_mbControlAppIP = NULL;
         SOCKET l_Sock;
         sockaddr_in l_server;
         int l_SentDataSize;
         int l_DataRemainderSize;
         SENDDATA l_SendData;
         PSENDDATA l_pSendData = &l_SendData;
         CopyMemory(&l_SendData, pResultData, sizeof(SENDDATA));

         if(l_pSendData == NULL)
         {
              return FALSE;
         }
         //IPをchar型に変換
         l_mbControlAppIP = _com_util::ConvertBSTRToString(ControlAppIP);
         //ソケットの準備
         l_Sock = socket(AF_INET, SOCK_STREAM, 0);

         l_server.sin_family = AF_INET;
         l_server.sin_port = htons(4463);
         l_server.sin_addr.S_un.S_addr = inet_addr(l_mbControlAppIP);

         //何度もこの関数を実行していると、途中からここで必ず失敗するようになります
         connect(l_Sock, (sockaddr *)&l_server, sizeof(l_server)) != 0)

         //connectできたら接続データ送信
         l_DataRemainderSize = sizeof(SENDDATA);
         do
         {
              l_SentDataSize = send(l_Sock, (const char *)l_pSendData, l_DataRemainderSize, 0);
              l_pSendData += l_SentDataSize;
              l_DataRemainderSize -= l_SentDataSize;
         }while(l_DataRemainderSize > 0);

         shutdown(l_Sock, SD_SEND);
         closesocket(l_Sock);

         if(l_mbControlAppIP != NULL)
         {
              delete[] l_mbControlAppIP;
              l_mbControlAppIP = NULL;
         }
         return TRUE;
    }

    setsockoptは、bindを実行しているサーバ側の方だけで呼び出せばいいんですよね?
    クライアント側でも必要なのでしょうか?

    2011年2月16日 5:58
  • 私も気になったので書きこまさせていただきます。

    WindowsのTIME WAITは、ソケットをクローズしても、2分~3分残ってしまいます。

    レジストリを変更することでこの値を小さくすることができますが。

    これとは関係ないことなのでしょうか?

    レジストリでいいますと、TcpTimeWaitDelayです。

    また、接続ができる最大数は、MaxUserPortで変更できると思います。

    2011年2月16日 9:16
  • dsa_maさん

    レスありがとうございます。
    setsockopt関数で、SO_REUSEADDRに設定しアドレスの再利用さえできればよいのだと思っていました^^;
    まだまだ勉強不足ですね、私>_<

    確かに、かなり小刻みにTCPによるパケットのやり取りを行っているので、レジストリを調整して試してみます。

    結果が出ましたら、またご報告します。

    ちなみに、connect関数でコケている時のエラーを拾ってみたところ10061(Connection refused)でした。
    この時にクライアントのアプリケーションを落としあげしても状況は変わらず、サーバアプリの落としあげをすると、通信が再開するので、サーバ側のレジストリをいじってみようと思います。

    結果が出ましたら、またご報告いたします。

    2011年2月16日 9:28
  • サーバ側、クライアント側両方とも、タスクマネージャなどで、起動時と、不具合が起きた時の差をみてみてはどうでしょうか?

    通信以外のところで、ハンドル数やスレッド数などが、異常に増えているなどがあるかもしれません。

    2011年2月16日 12:28
  • yminetさん

    レスありがとうございます。
    dsa_maさんにご教授いただいたレジストリの設定をしてみたのですが、状況は変わりませんでした。
    その際、ハンドルやスレッドの数も確認してみましたが、特に増えているわけでもなく…

    ソケットの作成からループの中に入れるなどしたほうがいいんですかね?
    Winsockそこまで詳しくなくて、手探り状態で…^^;

    数時間たつとクライアント側でconnect関数で10061(Connection refused)になり、そのまま継続してconnectが失敗してしまいます。

    サーバ上で、基本的に常時稼働(24h365dとまではいかないでしょうけど、そのくらいのつもりで…)させたいアプリなので、ここをなんとしてもクリアしないとなんです。

    どんな些細のヒントでも構いませんので、他に何か疑わしいものがありましたらお願いいたします。

    2011年2月18日 2:56
  • 直接の解決策になるかどうか分かりませんが、二点コメントします。

    ・Graceful Shutdown について

     http://msdn.microsoft.com/en-us/library/ms738547(VS.85).aspx

    にある表の、クライアント側での振る舞いとして、「shutdown() の後、切断(FD_CLOSE)を検出するまで recv() を呼び出す。その後、closesocket() を呼ぶ」と書かれていますよね。ご提示のコードでは、クライアント側での、shutdown() と closesocket() の間での recv() の呼び出しがありません。
    その処理を入れて改善するかどうかは分かりませんが、次の二番目のコメントの方策を試す手間が大きい場合は、念のため、一度試してみて下さいませ。


    ・コネクションの開設・切断回数を減らす。
    コネクションを短時間に大量に開設・切断することが要因となっていますので、その回数を減らすことで改善できると思います。
    具体的には、次の改訂を加えるのは、いかがでしょうか:

     - クライアント側の SendResult() 内部で connect() せず、SendResult() の呼び出し側で connect() してソケットを渡すようにする。

     - SendResult() の呼び出し側では、送出するデータが一定時間以上ない場合(※一定時間以上、SendResult() を呼び出す必要がない場合)、サーバとのコネクションを切断する。コネクションを切断していない場合は、SendResult() を呼び出す際、それを再利用する。

    SendResult() を呼び出す側の処理の改訂が若干多いかも知れませんが、これが根本的な解決策ではないかと思います。

    • 回答としてマーク どらちん 2011年2月23日 2:18
    2011年2月18日 3:59
  • 古賀信哉さん

    レスありがとうございます!!

    >クライアント側での振る舞いとして、「shutdown() の後、切断(FD_CLOSE)を検出するまで recv() を呼び出す。
    >その後、closesocket() を呼ぶ」と書かれていますよね。ご提示のコードでは、クライアント側での、shutdown() と closesocket() の間での recv() の呼び出しがありません。

    おっしゃる通りですね。
    恥ずかしながら、見落としていました。

    そこで、WSAEventSelect関数で

    HANDLE l_hCloseEvent;
    WSAEventSelect(l_Sock, l_hCloseEvent, FD_CLOSE);

    と、切断のイベントを取得したときのハンドルを作成し、WSAWaitForMultipleEvents関数で、shutodounでSD_SENDをした後に待機しようとしてみたのですが…

    WSAEventSelectで10022(引数が不正?)と出てしまうんです。
    なぜでしょうか?

    修正したクライアント側のソースを記載してみます(前回同様、修正個所を太字にしてみました)

    //クライアント側からサーバへデータを送信する関数
    //PSENDDATAは、サーバ側で利用しているSENDDATA構造体のポインタです。
    BOOL SendResult(PSENDDATA pResultData, BSTR ControlAppIP)
    {
         char *l_mbControlAppIP = NULL;
         SOCKET l_Sock;
         sockaddr_in l_server;
         int l_SentDataSize;
         int l_DataRemainderSize;
         SENDDATA l_SendData;
         PSENDDATA l_pSendData = &l_SendData;
         CopyMemory(&l_SendData, pResultData, sizeof(SENDDATA));

         HANDLE l_hCloseEvent = NULL;

         if(l_pSendData == NULL)
         {
              return FALSE;
         }
         //IPをchar型に変換
         l_mbControlAppIP = _com_util::ConvertBSTRToString(ControlAppIP);
         //ソケットの準備
         l_Sock = socket(AF_INET, SOCK_STREAM, 0);

         l_server.sin_family = AF_INET;
         l_server.sin_port = htons(4463);
         l_server.sin_addr.S_un.S_addr = inet_addr(l_mbControlAppIP);

         WSAEventSelect(l_Sock, l_hCloseEvent, FD_CLOSE);

         //何度もこの関数を実行していると、途中からここで必ず失敗するようになります
         connect(l_Sock, (sockaddr *)&l_server, sizeof(l_server)) != 0)

         //connectできたら接続データ送信
         l_DataRemainderSize = sizeof(SENDDATA);
         do
         {
              l_SentDataSize = send(l_Sock, (const char *)l_pSendData, l_DataRemainderSize, 0);
              l_pSendData += l_SentDataSize;
              l_DataRemainderSize -= l_SentDataSize;
         }while(l_DataRemainderSize > 0);

         shutdown(l_Sock, SD_SEND);
         WSAWaitForMultipleEvents(1, &l_hCloseEvent, TRUE,  WSA_INFINITE, FALSE);
         closesocket(l_Sock);

         WSACloseEvent(l_hCloseEvent);
         if(l_mbControlAppIP != NULL)
         {
              delete[] l_mbControlAppIP;
              l_mbControlAppIP = NULL;
         }
         return TRUE;
    }


    何が良くないのでしょう?

    2011年2月18日 6:15
  • l_hCloseEventのハンドルがNULLだからではないでしょうか?

    HANDLE l_hCloseEvent = WSACreateEvent();

     

    • 回答としてマーク どらちん 2011年2月23日 2:17
    2011年2月18日 7:13
  • dsa_maさん

    >l_hCloseEventのハンドルがNULLだからではないでしょうか?

    …あ^^;
    忘れてました>_<

    修正してまたランニングテストしてみます!!

    2011年2月18日 8:34
  • アドバイスをくださった皆様

    おかげさまで、connectでエラーが発生することなく動作することができました。
    また別の問題(というか気になること)が発生しましたが、本件とは全く異なる内容なので、別スレッドを立てさせていただこうと思います。

    本当に、ありがとうございました!!

    2011年2月23日 2:17