Microsoft Visual C++ 2010 Express

■TCPプロトコルでクライアント/サーバー通信 Prev Top Next
関連ページ:なし
2010/10/17:
  1. 受信処理にタイムアウトを設定


今回はTCPプロトコルを使用し、クライアント/サーバーモデルで通信してみます。 やってることは至極単純でクライアントからサーバーに数値電文を送信し、サーバー側で受信した数値電文を負の数に変換してクライアントに送り返すだけです。 複数のクライアントから接続できないなど、機能不足のためいろいろ問題あります。気が向いたら修正しますが、たぶんやらない。

今回のサンプル作成では下記のサイトを参照しました。

Windows Sockets 2

戸谷 浩史さんが運営するサイトです。このサイトすごい。VC++の通信系API関数のリファレンスやサンプルソースやらと、いたれりつくせりです。 ネットワークプログラミングの勉強するのであれば、Windows Sockets 2を参照することをお勧めします。

ではソースを見ていきますが、最初にいくつか注意点。ネットワーク系のサンプルソースはマルチバイト文字セットを使用します。何故かはめんどくさいからです。 ですのでプロジェクトのプロパティの文字セットをマルチバイト文字セットに設定してください。

次にこのページのサンプルではサーバー側のプログラムに 50000番 のポート番号を割り当てています。 実行中の他のプログラムが使用中の場合は 50000番 をあけるか、またはプログラムを修正して未使用の別のポート番号を割り当ててください。

---main.cpp ( サーバー側 )---


#include <stdio.h>
#include <WINSOCK2.h>

#pragma comment( lib, "WS2_32.lib" )

// アプリケーションが要求するバージョン
#define MAJOR_VERSION 2
#define MINOR_VERSION 0
// ポート番号。未使用のポート番号を指定すること
#define PORT          50000
// データ量の最大値は SO_MAX_MSG_SIZE
#define MESSAGE_SIZE  1024

int APIENTRY WinMain( HINSTANCE hInstance,
                      HINSTANCE /*hPrevInstance*/,
                      LPTSTR    /*lpCmpLine*/,
                      INT       /*nCmdShow*/ )
{
   int hr = -1;

   // WSAStartup() の呼び出し回数
   int WSAStartupCount = 0;

   // ソケット識別子
   SOCKET ListenID = INVALID_SOCKET, SocketID = INVALID_SOCKET;

   // 送受信バッファ
   char SendBuffer[MESSAGE_SIZE], RecvBuffer[MESSAGE_SIZE];

   // デバッガ出力用バッファ
   TCHAR OutString[512];

   // WS2_32.dllの初期化
   {
      // WSADATA 構造体
      WSADATA wsd;
   
      // WS2_32.dllの初期化
      // WSAStartup Function
      hr = WSAStartup( MAKEWORD( MAJOR_VERSION, MINOR_VERSION ), &wsd );
      if( hr != 0 )
      {
         // Windows Sockets Error Codes
         sprintf_s( OutString, "WSAStartup()でエラー [ ErrorCode:%d ]\n", hr );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
      WSAStartupCount++;

      // WSADATA::wVersion には、WSAStartup() に指定したバージョン以下でライブラリがサポートする最大バージョンが格納される
      // ここでは完全に一致する場合のみ動作するようにする
      if( LOBYTE( wsd.wVersion ) != MAJOR_VERSION ||
          HIBYTE( wsd.wVersion ) != MINOR_VERSION )
      {
         sprintf_s( OutString,
                    "ライブラリがサポートするバージョンは[ %d.%d ]ですがアプリが要求するバージョンは[ %d.%d ]です\n",
                     LOBYTE( wsd.wVersion ), HIBYTE( wsd.wVersion ), MAJOR_VERSION, MINOR_VERSION );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
   }

   // クライアントからの接続待ち用のソケットの作成
   {
      // ソケットの作成
      // WSASocket Function
      ListenID = WSASocket( AF_INET,
                            SOCK_STREAM,   // 接続型ソケット
                            IPPROTO_TCP,   // TCPプロトコルを指定する
                            NULL,
                            0,
                            0
                          );
      if( ListenID == INVALID_SOCKET )
      {
         sprintf_s( OutString, "WSASocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }

      // 接続情報を設定する構造体
      // SOCKADDR_IN 構造体
      SOCKADDR_IN ListenAddr;

      ::ZeroMemory( &ListenAddr, sizeof(SOCKADDR_IN) );

      // 通常 AF_INET に設定する
      ListenAddr.sin_family = AF_INET;

      // 自分自身のIPアドレスを設定する。
      // htonl についてはバイトオーダーで説明されている
      ListenAddr.sin_addr.s_addr = htonl( INADDR_ANY );

      // ポート番号指定
      // htons についてはバイトオーダーで説明されている
      ListenAddr.sin_port = htons( PORT );

      // ソケットとローカル アドレスを関連づける
      hr = bind( ListenID,
                 (SOCKADDR*)&ListenAddr,
                 sizeof(ListenAddr)
               );
      if( hr != 0 )
      {
         sprintf_s( OutString, "bind()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
   }

   // クライアントからの接続待ち行列を設定
   hr = listen( ListenID,
                0   // 接続要求の処理待ち配列の最大要素数を設定。SOMAXCONN に設定すると適切な最大値に設定する。
              );
   if( hr != 0 )
   {
      sprintf_s( OutString, "listen()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
      OutputDebugString( OutString );
      hr = -1;
      goto EXIT;
   }

   sprintf_s( OutString, "listen()が成功(^_^)\n" );
   OutputDebugString( OutString );

   while( true )
   {
      // クライアントから接続待ち
      {
         SOCKADDR_IN SocketAddr;
         int AddrSize = sizeof(SocketAddr);

         // クライアントから接続要求が発生するまでここでブロックされる。
         // なお SocketAddr に接続してきたクライアントのIPアドレスとポート番号が格納される。
         // WSAAccept Function
         SocketID = WSAAccept( ListenID,
                               (SOCKADDR*)&SocketAddr,
                               &AddrSize,
                               NULL,
                               0
                             );
         if( SocketID == INVALID_SOCKET )
         {
            sprintf_s( OutString, "WSAAccept()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
            OutputDebugString( OutString );
            hr = -1;
            goto EXIT;
         }
      }

      while( true )
      {
         RecvBuffer[0] = '\0';
         SendBuffer[0] = '\0';

         // データを受信するまでブロック
         {
            struct fd_set readfds;
            FD_ZERO( &readfds ) ;
            FD_SET( SocketID , &readfds );

            struct timeval timeout;
            timeout.tv_sec = 10;  // 10秒待つ
            timeout.tv_usec = 0;

            // select Function
            // ソケットの状態を設定する。ここでは読取り用のソケットに対してタイムアウトを指定する。
            int n = select( 
                            0,             // 未使用っぽい
                            &readfds,      // ソケットが読取り可能かをチェックするようにする
                            NULL,
                            NULL,
                            &timeout       // タイムアウト時間
                          );
            if( n == SOCKET_ERROR )
            {
               sprintf_s( OutString, "select()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
               OutputDebugString( OutString );
               hr = -1;
               goto EXIT;
            }
            // タイムアウト発生
            if( n == 0 )
            {
               OutputDebugString( "タイムアウトしました。\n" );
               break;
            }
         }

         // クライアントから受信
         {
            WSABUF buf;
            buf.buf = RecvBuffer;
            buf.len = MESSAGE_SIZE;

            DWORD RecvBytes;

            // クライアントから受信
            // WSARecv Function
            hr = WSARecv( SocketID,
                          &buf,
                          1,
                          &RecvBytes,
                          0,
                          NULL,
                          NULL
                        );
            if( hr != 0 )
            {
               sprintf_s( OutString, "WSARecv()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
               OutputDebugString( OutString );
               break;
            }
            else
            {
               if( RecvBytes == 0 )
               {
                  OutputDebugString( "クライアントからシャットダウンを受信\n" );
                  break;
               }
            }
         }

         // クライアントに送信
         {
            // 受信データを負の数に変換する
            sprintf_s( SendBuffer, "-%s", RecvBuffer );
            WSABUF buf;
            buf.buf = SendBuffer;
            buf.len = MESSAGE_SIZE;

            DWORD SendBytes;

            // クライアントに送信
            // WSASend Function
            hr = WSASend( SocketID,
                          &buf,
                          1,
                          &SendBytes,
                          0,
                          NULL,
                          NULL
                        );
            if( hr != 0 )
            {
               sprintf_s( OutString, "WSASend()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
               OutputDebugString( OutString );
               break;
            }
         }

         SYSTEMTIME st;
         ::GetLocalTime( &st );
         sprintf_s( OutString,
                    "%d:%d:%d:%d [ 受信:%s ][ 送信:%s ]\n",
                    st.wHour, st.wSecond, st.wMinute, st.wMilliseconds, RecvBuffer, SendBuffer );
         OutputDebugString( OutString );
      }

      // 切断処理
      if( SocketID != INVALID_SOCKET )
      {
         if( closesocket( SocketID ) != 0 )
         {
            sprintf_s( OutString, "closesocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
            OutputDebugString( OutString );
         }

         SocketID = INVALID_SOCKET;

         OutputDebugString( "サーバー側切断完了\n" );
      }
   }

   hr = 0;

EXIT:

   // ソケットの切断
   if( SocketID != INVALID_SOCKET )
   {
      if( closesocket( SocketID ) != 0 )
      {
         sprintf_s( OutString, "closesocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
      }
   }

   // ソケットの切断
   if( ListenID != INVALID_SOCKET )
   {
      if( closesocket( ListenID ) != 0 )
      {
         sprintf_s( OutString, "closesocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
      }
   }

   // WS2_32.dllの開放
   // WSACleanup Function
   for( int i=0; i<WSAStartupCount; i++ )
   {
      if( WSACleanup() != 0 )
      {
         sprintf_s( OutString, "WSACleanup()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
      }
   }

   return hr;
}

受信処理にタイムアウトを設定するように修正しました。修正箇所は select() 関数を使用しているあたりです。

これは WSARecv() で受信待ち状態に入った後、通信障害などの理由で、 クライアントから電文が届かない状態に陥った場合、サーバー側はいつまでも電文受信待ち状態を続けることになるためです。

この対策としてタイムアウトを設定して、10秒以上経過したらソケットを強制的にクローズするようにします。

---main.cpp ( クライアント側 )---


#include <stdio.h>
#include <WINSOCK2.h>

#pragma comment( lib, "WS2_32.lib" )

// アプリケーションが要求するバージョン
#define MAJOR_VERSION 2
#define MINOR_VERSION 0
// サーバーのIPアドレス。127.0.0.1は特殊なアドレスで自分自身を示す。
// サンプルでは1台のPC上でテストしたため、このIPアドレスを指定する。
#define IP            "127.0.0.1"
// サーバー側のポート番号。
#define PORT          50000
// データ量の最大値は SO_MAX_MSG_SIZE
#define MESSAGE_SIZE  1024

int APIENTRY WinMain( HINSTANCE hInstance,
                      HINSTANCE /*hPrevInstance*/,
                      LPTSTR    /*lpCmpLine*/,
                      INT       /*nCmdShow*/ )
{
   int hr = -1;

   // WSAStartup() の呼び出し回数
   int WSAStartupCount = 0;

   // ソケット識別子
   SOCKET SocketID = INVALID_SOCKET;

   // 送受信バッファ
   char SendBuffer[MESSAGE_SIZE], RecvBuffer[MESSAGE_SIZE];

   // デバッガ出力用バッファ
   TCHAR OutString[512];

   // WS2_32.dllの初期化
   {
      // WSADATA 構造体
      WSADATA wsd;
   
      // WS2_32.dllの初期化
      // WSAStartup Function
      hr = WSAStartup( MAKEWORD( MAJOR_VERSION, MINOR_VERSION ), &wsd );
      if( hr != 0 )
      {
         // Windows Sockets Error Codes
         sprintf_s( OutString, "WSAStartup()でエラー [ ErrorCode:%d ]\n", hr );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
      WSAStartupCount++;

      // WSADATA::wVersion には、WSAStartup() に指定したバージョン以下でライブラリがサポートする最大バージョンが格納される
      if( LOBYTE( wsd.wVersion ) != MAJOR_VERSION ||
          HIBYTE( wsd.wVersion ) != MINOR_VERSION )
      {
         sprintf_s( OutString,
                    "ライブラリがサポートするバージョンは[ %d.%d ]ですがアプリが要求するバージョンは[ %d.%d ]です\n",
                    LOBYTE( wsd.wVersion ), HIBYTE( wsd.wVersion ), MAJOR_VERSION, MINOR_VERSION );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
   }

   // サーバーと接続するためのソケットの作成
   {
      // ソケットの作成
      // WSASocket Function
      SocketID = WSASocket( AF_INET,
                            SOCK_STREAM,   // 接続型ソケット
                            IPPROTO_TCP,   // TCPプロトコルを指定する
                            NULL,
                            0,
                            0
                          );
      if( SocketID == INVALID_SOCKET )
      {
         sprintf_s( OutString, "WSASocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }

      // 接続情報を設定する構造体
      // SOCKADDR_IN 構造体
      SOCKADDR_IN SockAddr;

      ::ZeroMemory( &SockAddr, sizeof(SOCKADDR_IN) );

      // 通常 AF_INET に設定する
      SockAddr.sin_family = AF_INET;

      // inet_addr は.つきのアドレス形式を数値に変換する
      SockAddr.sin_addr.s_addr = inet_addr( IP );

      // ポート番号指定
      // htons についてはバイトオーダーで説明されている
      SockAddr.sin_port = htons( PORT );

      // サーバーのIPアドレスとポート番号を指定して接続
      hr = WSAConnect( SocketID,
                       (SOCKADDR*)&SockAddr,
                       sizeof(SockAddr),
                       NULL,
                       NULL,
                       NULL,
                       NULL
                     );
      if( hr != 0 )
      {
         sprintf_s( OutString, "WSAConnect()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
         hr = -1;
         goto EXIT;
      }
   }

   sprintf_s( OutString, "%sに接続(^_^)\n", IP );
   OutputDebugString( OutString );

   while( 1 )
   {
      RecvBuffer[0] = '\0';
      SendBuffer[0] = '\0';

      // サーバーへ電文送信
      {
         // 送信電文を作成
         static DWORD data = 0;
         sprintf_s( SendBuffer, "%d", data );
         data ++;

         WSABUF buf;
         buf.buf = SendBuffer;
         buf.len = MESSAGE_SIZE;

         DWORD SendBytes;

         // サーバーに送信
         // WSASend Function
         hr = WSASend( SocketID,
                       &buf,
                       1,
                       &SendBytes,
                       0,
                       NULL,
                       NULL
                     );
         if( hr != 0 )
         {
            sprintf_s( OutString, "WSASend()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
            OutputDebugString( OutString );
            hr = -1;
            goto EXIT;
         }
      }

      // データを受信するまでブロック
      {
         struct fd_set readfds;
         FD_ZERO( &readfds ) ;
         FD_SET( SocketID , &readfds );

         struct timeval timeout;
         timeout.tv_sec = 10;  // 10秒待つ
         timeout.tv_usec = 0;

         // select Function
         // ソケットの状態を設定する。ここでは読取り用のソケットに対してタイムアウトを指定する。
         int n = select( 
                         0,             // 未使用っぽい
                         &readfds,      // ソケットが読取り可能かをチェックするようにする
                         NULL,
                         NULL,
                         &timeout       // タイムアウト時間
                       );
         if( n == SOCKET_ERROR )
         {
            sprintf_s( OutString, "select()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
            OutputDebugString( OutString );
            hr = -1;
            goto EXIT;
         }
         // タイムアウト発生
         if( n == 0 )
         {
            OutputDebugString( "タイムアウトしました。\n" );
            break;
         }
      }

      // サーバーから電文受信
      {
         WSABUF buf;
         buf.buf = RecvBuffer;
         buf.len = MESSAGE_SIZE;

         DWORD RecvBytes;

         // サーバーから受信
         // WSARecv Function
         hr = WSARecv( SocketID,
                       &buf,
                       1,
                       &RecvBytes,
                       0,
                       NULL,
                       NULL
                     );
         if( hr != 0 )
         {
            sprintf_s( OutString, "WSARecv()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
            OutputDebugString( OutString );
            hr = -1;
            goto EXIT;
         }
      }

      SYSTEMTIME st;
      ::GetLocalTime( &st );
      sprintf_s( OutString,
                 "%d:%d:%d:%d [ 送信:%s ][ 受信:%s ]\n",
                 st.wHour, st.wSecond, st.wMinute, st.wMilliseconds, SendBuffer, RecvBuffer );
      OutputDebugString( OutString );
   }

   hr = 0;

EXIT:

   // ソケットの切断
   if( SocketID !=INVALID_SOCKET )
   {
      if( closesocket( SocketID ) != 0 )
      {
         sprintf_s( OutString, "closesocket()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
      }
   }

   // WS2_32.dllの開放
   // WSACleanup Function
   for( int i=0; i<WSAStartupCount; i++ )
   {
      if( WSACleanup() != 0 )
      {
         sprintf_s( OutString, "WSACleanup()でエラー [ ErrorCode:%d ]\n", WSAGetLastError() );
         OutputDebugString( OutString );
      }
   }

   return hr;
}

サーバー側アプリを実行してからクライアント側アプリを実行してください。
またウィンドウは作成していません。実行結果はデバックウィンドウに表示されます。


Prev Top Next
inserted by FC2 system