Microsoft Visual C++ 2010 Express |
■TCPプロトコルでクライアント/サーバー通信 | Prev Top Next |
関連ページ:なし |
|
今回のサンプル作成では下記のサイトを参照しました。
戸谷 浩史さんが運営するサイトです。このサイトすごい。VC++の通信系API関数のリファレンスやサンプルソースやらと、いたれりつくせりです。 ネットワークプログラミングの勉強するのであれば、Windows Sockets 2を参照することをお勧めします。
ではソースを見ていきますが、最初にいくつか注意点。ネットワーク系のサンプルソースはマルチバイト文字セットを使用します。何故かはめんどくさいからです。 ですのでプロジェクトのプロパティの文字セットをマルチバイト文字セットに設定してください。
次にこのページのサンプルではサーバー側のプログラムに 50000番 のポート番号を割り当てています。 実行中の他のプログラムが使用中の場合は 50000番 をあけるか、またはプログラムを修正して未使用の別のポート番号を割り当ててください。
#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秒以上経過したらソケットを強制的にクローズするようにします。
#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; }サーバー側アプリを実行してからクライアント側アプリを実行してください。