EternalWindows
Winsock / データの送受信(WSA編)
対応するサーバーはこちら

前節のクライアントプログラムでは、ioctlsocketでソケットを非ブロッキングモードに設定し、 recvを一定間隔で呼び出すという実装になっていました。 このような実装にした理由は、recvの呼び出しでブロッキングしてしまっては、 スレッドが終了フラグの値を確認することができなくなるからです。 しかしながら、この問題はWSA(Windows Sockets API)のWSAWaitForMultipleEventsを使用すれば簡単に解決することができます。 この関数は、単一または複数のイベントオブジェクトがシグナル状態になるまでブロッキングするため、 終了フラグをTRUEにする代わりに、イベントオブジェクトをシグナル状態に変更すればよいことになります。

DWORD WSAWaitForMultipleEvents(
  DWORD cEvents,
  const WSAEVENT *lphEvents,
  BOOL fWaitAll,
  DWORD dwTimeout,
  BOOL fAlertable
);

cEventsは、lphEventsの要素数を指定します。 lphEventsは、イベントオブジェクトの配列を指定します。 fWaitAllは、どのように待機するかを指定します。 TRUEの場合は配列内のすべてのイベントオブジェクトがシグナル状態になるまで待機し、 FALSEの場合はいずれかのイベントオブジェクトがシグナル状態になるまで待機します。 dwTimeoutは、待機する時間をミリ秒単位で指定します。 WSA_INFINITEを指定すると、タイムアウトによって関数が制御を返すことはなくなります。 fAlertableは、アラータブルI/Oに関する引数ですが、特に利用することはないためFALSEで構いません。

接続や読み取りといったネットワークイベントをイベントオブジェクトに関連付けている場合、 このイベントオブジェクトをWSAWaitForMultipleEventsに指定することで、 ネットワークイベントが発生したタイミングを特定することができます。 ネットワークイベントをイベントオブジェクトに関連付けるには、WSAEventSelectを呼び出します。

int WSAEventSelect(
  SOCKET s,
  WSAEVENT hEventObject,
  long lNetworkEvents
);

sは、ソケットの記述子を指定します。 hEventObjectは、ネットワークイベントを関連付けたいイベントオブジェクトを指定します。 lNetworkEventsは、ネットワークイベントとして定義されている定数を指定します。

指定したソケットにネットワークイベントが発生して、 WSAWaitForMultipleEventsが制御を返した場合、 その発生したネットワークイベントの種類を特定する必要があります。 これには、WSAEnumNetworkEventsを呼び出します。

int WSAEnumNetworkEvents(
  SOCKET s,
  WSAEVENT hEventObject,
  LPWSANETWORKEVENTS lpNetworkEvents
);

sは、ソケットの記述子を指定します。 hEventObjectは、イベントオブジェクトを指定します。 lpNetworkEventsは、発生したネットワーイベント格納するWSANETWORKEVENTS構造体のアドレスを指定します。

今回のクライアントプログラムは、対応するサーバープログラムにデータを送信します。 これを取得したサーバーはクライアントに応答データを送信し、 クライアントはこれを別スレッドで受信します。 事前に、対応するサーバープログラムを起動しておいてください。

#include <winsock2.h>
#include <ws2tcpip.h>

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

#define ID_SEND 100
#define ID_EDIT 200

WSAEVENT g_hEventExit = NULL;

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort);
DWORD WINAPI ThreadProc(LPVOID lpParamater);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample-client");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND   hwndButton = NULL;
	static HWND   hwndEdit = NULL;
	static HANDLE hThread = NULL;
	static SOCKET soc = INVALID_SOCKET;
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD dwThreadId;
		
		hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("送信"), WS_CHILD | WS_VISIBLE, 10, 10, 60, 30, hwnd, (HMENU)ID_SEND, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndEdit = CreateWindowEx(0, TEXT("EDIT"), TEXT("メッセージを入力してください。"), WS_CHILD | WS_VISIBLE | WS_BORDER, 90, 10, 300, 35, hwnd, (HMENU)ID_EDIT, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		soc = InitializeWinsock("localhost", "5000");
		if (soc == INVALID_SOCKET)
			return -1;
		
		g_hEventExit = WSACreateEvent();

		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, &soc, 0, &dwThreadId);

		return 0;
	}

	case WM_COMMAND: {
		int   nLen;
		int   nResult;
		TCHAR szData[256];

		if (LOWORD(wParam) != ID_SEND)
			return 0;

		nLen = GetWindowText(hwndEdit, szData, sizeof(szData));
		nLen = (nLen + 1) * sizeof(TCHAR);

		nResult = send(soc, (char *)szData, nLen, 0);
		if (nResult == SOCKET_ERROR)
			MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

		return 0;
	}

	case WM_DESTROY:
		if (hThread != NULL) {
			WSASetEvent(g_hEventExit);
			WaitForSingleObject(hThread, 1000);
			CloseHandle(hThread);
			WSACloseEvent(g_hEventExit);
		}

		if (soc != INVALID_SOCKET) {
			shutdown(soc, SD_BOTH);
			closesocket(soc);
			WSACleanup();
		}

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	SOCKET           soc = *((SOCKET *)lpParamater);
	DWORD            dwResult;
	WSAEVENT         hEvent;
	WSAEVENT         hEvntArray[2];
	WSANETWORKEVENTS events;
	
	hEvent = WSACreateEvent();
	WSAEventSelect(soc, hEvent, FD_READ | FD_CLOSE);

	hEvntArray[0] = hEvent;
	hEvntArray[1] = g_hEventExit;

	for (;;) {
		dwResult = WSAWaitForMultipleEvents(2, hEvntArray, FALSE, WSA_INFINITE, FALSE);
		if (dwResult == WSA_WAIT_FAILED)
			break;

		if (dwResult - WSA_WAIT_EVENT_0 == 0) {
			WSAEnumNetworkEvents(soc, hEvent, &events);

			if (events.lNetworkEvents & FD_CLOSE) {
				MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
				break;
			}
			else if (events.lNetworkEvents & FD_READ) {
				int   nLen;
				int   nResult;
				TCHAR szBuf[256];
				TCHAR szData[256];

				nLen = sizeof(szData);
				nResult = recv(soc, (char *)szData, nLen, 0);
				wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
				MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
			}
			else
				;
			WSAResetEvent(hEvent);
		}
		else if (dwResult - WSA_WAIT_EVENT_0 == 1)
			break;
		else
			;
	}
	
	WSACloseEvent(hEvent);

	return 0;
}

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     soc;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family   = AF_INET;
	addrHints.ai_socktype = SOCK_STREAM;
	addrHints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(lpszServerName, lpszPort, &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return INVALID_SOCKET;
	}

	soc = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);

	if (connect(soc, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		int nError = WSAGetLastError();
		if (nError == WSAECONNREFUSED)
			MessageBox(NULL, TEXT("サーバーがリッスンしていません。"), NULL, MB_ICONWARNING);
		else if (nError == WSAEHOSTUNREACH)
			MessageBox(NULL, TEXT("サーバーが存在しません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サーバーへの接続が失敗しました。"), NULL, MB_ICONWARNING);
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	freeaddrinfo(lpAddrList);

	return soc;
}

今回のThreadProcでは、WSAWaitForMultipleEventsを呼び出すことによって、 スレッドの終了とソケットの変化の両方を検出するようにしています。 関数の処理を順に見ていきます。

hEvent = WSACreateEvent();
WSAEventSelect(soc, hEvent, FD_READ | FD_CLOSE);

hEvntArray[0] = hEvent;
hEvntArray[1] = g_hEventExit;

まず、イベントオブジェクトを作成し、WSAEventSelectでソケットを関連付けるようにします。 FD_READとFD_CLOSEを指定しているため、 データが届いた場合か相手が切断した場合に、イベントオブジェクトがシグナル状態になります。 hEvntArrayは、イベントオブジェクトの配列であり、 ここには先に作成したイベントと、スレッドの終了を表すイベントを指定します。 後者のイベントは、WM_CREATEで作成されています。 初期化された配列は、WSAWaitForMultipleEventsに指定されることになります。

dwResult = WSAWaitForMultipleEvents(2, hEvntArray, FALSE, WSA_INFINITE, FALSE);
if (dwResult == WSA_WAIT_FAILED)
	break;

第3引数にFALSEを指定していることから、配列に格納されている2つのイベントオブジェクトのうち、 1つがシグナル状態になった時点で関数が制御を返すことになります。 戻り値からWSA_WAIT_EVENT_0を引いた値は、シグナル状態になったイベントオブジェクトのインデックスになっているため、 これがたとえば0であるならば、hEvntArray[0]のイベントオブジェクトがシグナル状態になったことを意味します。 関数が失敗した場合はWSA_WAIT_FAILEDが返るため、この場合はループから抜けるようにしています。

if (dwResult - WSA_WAIT_EVENT_0 == 0) {
	WSAEnumNetworkEvents(soc, hEvent, &events);

	if (events.lNetworkEvents & FD_CLOSE) {
		MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
		break;
	}
	else if (events.lNetworkEvents & FD_READ) {
		int   nLen;
		int   nResult;
		TCHAR szBuf[256];
		TCHAR szData[256];

		nLen = sizeof(szData);
		nResult = recv(soc, (char *)szData, nLen, 0);
		wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}
	else
		;
	WSAResetEvent(hEvent);
}

このif文は、ソケットが関連付けられたイベントオブジェクトがシグナル状態になった場合に実行されます。 まず、WSAEnumNetworkEventsでどのようなイベントが発生したのかを取得し、 lNetworkEventsメンバの値を確認することになります。 FD_CLOSEが含まれている場合は通信相手が切断したことを意味し、 FD_READの場合はデータが届いていることを意味します。 データが既に届いているため、recvの呼び出しによってブロッキングが発生することはありません。 WSAResetEventを呼び出しているのは、シグナル状態になったイベントオブジェクトを非シグナル状態に戻すためです。

else if (dwResult - WSA_WAIT_EVENT_0 == 1)
	break;

このelse if文は、g_hEventExitがシグナル状態になった場合に実行されます。 この場合は、スレッドが終了しなければなりませんから、breakでループを抜けることになります。 g_hEventExitは、WM_DESTROYのWSASetEventによってシグナル状態に設定されています。

WSAAsyncSelectの利用

今回のプログラムは、WSAEventSelectを呼び出して実装されていますが、 スレッドやイベントオブジェクトを作成するという点で、少し複雑であるといえます。 次のコードのようにWSAAsyncSelectを呼び出せば、 メッセージを通じて処理のタイミングを確認することができます。

#include <winsock2.h>
#include <ws2tcpip.h>

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

#define ID_SEND 100
#define ID_EDIT 200
#define WM_SOCKET WM_APP

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample-client");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND   hwndButton = NULL;
	static HWND   hwndEdit= NULL;
	static SOCKET soc = INVALID_SOCKET;
	
	switch (uMsg) {

	case WM_CREATE:
		hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("送信"), WS_CHILD | WS_VISIBLE, 10, 10, 60, 30, hwnd, (HMENU)ID_SEND, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndEdit= CreateWindowEx(0, TEXT("EDIT"), TEXT("メッセージを入力してください。"), WS_CHILD | WS_VISIBLE | WS_BORDER, 90, 10, 300, 35, hwnd, (HMENU)ID_EDIT, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		soc = InitializeWinsock("localhost", "5000");
		if (soc == INVALID_SOCKET)
			return -1;

		WSAAsyncSelect(soc, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);

		return 0;

	case WM_COMMAND: {
		int   nLen;
		int   nResult;
		TCHAR szData[256];

		if (LOWORD(wParam) != ID_SEND)
			return 0;

		nLen = GetWindowText(hwndEditBox, szData, sizeof(szData));
		nLen = (nLen + 1) * sizeof(TCHAR);

		nResult = send(soc, (char *)szData, nLen, 0);
		if (nResult == SOCKET_ERROR)
			MessageBox(NULL, TEXT("データの送信に失敗しました。"), NULL, MB_ICONWARNING);

		return 0;
	}
	
	case WM_SOCKET:
		switch (WSAGETSELECTEVENT(lParam)) {

		case FD_READ: {
			int   nLen;
			int   nResult;
			TCHAR szBuf[256];
			TCHAR szData[256];
			
			nLen = sizeof(szData);
			nResult = recv(soc, (char *)szData, nLen, 0);
			
			wsprintf(szBuf, TEXT("%dバイト受信しました。\n%s"), nResult, szData);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
			break;
		}

		case FD_CLOSE:
			MessageBox(NULL, TEXT("サーバーとの接続が切断されました。"), TEXT("OK"), MB_OK);
			break;

		}
		return 0;

	case WM_DESTROY:
		if (soc != INVALID_SOCKET) {
			shutdown(soc, SD_BOTH);
			closesocket(soc);
			WSACleanup();
		}

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     soc;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family   = AF_INET;
	addrHints.ai_socktype = SOCK_STREAM;
	addrHints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(lpszServerName, lpszPort, &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return INVALID_SOCKET;
	}

	soc = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);

	if (connect(soc, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		int nError = WSAGetLastError();
		if (nError == WSAECONNREFUSED)
			MessageBox(NULL, TEXT("サーバーがリッスンしていません。"), NULL, MB_ICONWARNING);
		else if (nError == WSAEHOSTUNREACH)
			MessageBox(NULL, TEXT("サーバーが存在しません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サーバーへの接続が失敗しました。"), NULL, MB_ICONWARNING);
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	freeaddrinfo(lpAddrList);

	return soc;
}

WSAAsyncSelectの第4引数には、通知を望むイベントを指定します。 FD_READを指定すると、データが届いた段階で第3引数のメッセージが送られるため、 このときにrecvを呼び出してデータを取得することになります。 FD_CLOSEを指定した場合は、相手が切断した段階でメッセージが送られることになります。



戻る