EternalWindows
Winsock / データの送受信(VISTA編)
対応するクライアントはこちら

Windows Vistaから登場したWSAPollは、 指定したソケットが指定されたステータスに変化しているかどうかを確かめる機能を提供しています。 この関数は、次のように定義されています。

int WSAAPI WSAPoll(
  WSAPOLLFD fdarray[],
  ULONG nfds,
  INT timeout
);

fdarrayは、WSAPOLLFD構造体の配列を指定します。 nfdsは、fdarrayの要素数を指定します。 timeoutは、関数が制御を返すタイムアウト値を指定します。 0より大きい場合はその値だけ関数が待機し、0の場合は直ちに制御を返します。 負の値の場合は、ソケットが指定されたステータスに変化するまで制御を返しません。

今回のサーバープログラムは、対応するクライアントプログラムからデータを受信し、 それをリストボックスに表示します。 また、接続や切断が行われた時もリストボックスに表示します。

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

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

HWND g_hwndListBox = NULL;
BOOL g_bExitThread = FALSE;

SOCKET InitializeWinsock(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-server");
	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 HANDLE hThread = NULL;
	static SOCKET socListen = INVALID_SOCKET;
	
	switch (uMsg) {

	case WM_CREATE: {
		DWORD dwThreadId;
		
		g_hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		socListen = InitializeWinsock("7000");
		if (socListen == INVALID_SOCKET)
			return -1;
		
		hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, &socListen, 0, &dwThreadId);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(g_hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		if (hThread != NULL) {
			g_bExitThread = TRUE;
			WaitForSingleObject(hThread, 1000);
			CloseHandle(hThread);
		}

		if (socListen != INVALID_SOCKET) {
			closesocket(socListen);
			WSACleanup();
		}

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

DWORD WINAPI ThreadProc(LPVOID lpParamater)
{
	SOCKET    socListen = *((SOCKET *)lpParamater);
	int       i;
	int       nResult;
	int       nMaxSocketCount = 11;
	WSAPOLLFD fdArray[11];

	for (i = 0; i < nMaxSocketCount; i++) {
		fdArray[i].fd     = INVALID_SOCKET;
		fdArray[i].events = 0;
	}

	fdArray[0].fd     = socListen;
	fdArray[0].events = POLLRDNORM;

	while (!g_bExitThread) {
		nResult = WSAPoll(fdArray, nMaxSocketCount, 500);
		if (nResult < 0) {
			SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)TEXT("WSAPOLLFDの実行に失敗しました。"));
			break;
		}
		else if (nResult == 0)
			continue;
		else
			;

		for (i = 0; i < nMaxSocketCount; i++) {
			if (fdArray[i].revents & POLLRDNORM)
				break;
			else if (fdArray[i].revents & POLLHUP)
				break;
			else
				;
		}
		
		if (fdArray[i].revents & POLLRDNORM) {
			if (fdArray[i].fd == socListen) {
				int              nAddrLen;
				char             szBuf[256];
				char             szHostName[256];
				SOCKADDR_STORAGE sockAddr;

				for (i = 0; i < nMaxSocketCount; i++) {
					if (fdArray[i].fd == INVALID_SOCKET)
						break;
				}

				if (i == nMaxSocketCount)
					continue;

				nAddrLen = sizeof(SOCKADDR_STORAGE);
				fdArray[i].fd = accept(socListen, (LPSOCKADDR)&sockAddr, &nAddrLen);
				fdArray[i].events = POLLRDNORM;
				
				getnameinfo((LPSOCKADDR)&sockAddr, nAddrLen, szHostName, sizeof(szHostName), NULL, 0, 0);
				wsprintfA(szBuf, "No%d(%s) 接続", i, szHostName);
				SendMessageA(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
			}
			else {
				int   nLen;
				int   nResult;
				TCHAR szBuf[256];
				TCHAR szData[256];
				
				nLen = sizeof(szData);
				nResult = recv(fdArray[i].fd, (char *)szData, nLen, 0);

				wsprintf(szBuf, TEXT("No%d %s"), i, szData);
				SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

				nLen = nResult;
				nResult = send(fdArray[i].fd, (char *)szData, nLen, 0);
			}
		}
		else if (fdArray[i].revents & POLLHUP) {
			TCHAR szBuf[256];

			wsprintf(szBuf, TEXT("No%d 切断"), i);
			SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

			shutdown(fdArray[i].fd, SD_BOTH);
			closesocket(fdArray[i].fd);
			fdArray[i].fd = INVALID_SOCKET;
			fdArray[i].events = 0;
		}
		else
			;
	}

	return 0;
}

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

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

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

	socListen = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);
	
	if (bind(socListen, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		MessageBox(NULL, TEXT("ローカルアドレスとソケット関連付けに失敗しました。"), NULL, MB_ICONWARNING);
		closesocket(socListen);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	if (listen(socListen, 1) == SOCKET_ERROR) {
		closesocket(socListen);
		freeaddrinfo(lpAddrList);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	freeaddrinfo(lpAddrList);

	return socListen;
}

プログラムでは、クライアントからの接続を待つ手段、 及びクライアントからのデータの読み取りを検出する手段としてWSAPallを使用しています。 ThreadProcの内部を順に見ていきます。

int       nMaxSocketCount = 11;
WSAPOLLFD fdArray[11];

for (i = 0; i < nMaxSocketCount; i++) {
	fdArray[i].fd     = INVALID_SOCKET;
	fdArray[i].events = 0;
}

fdArray[0].fd     = socListen;
fdArray[0].events = POLLRDNORM;

fdArrayの要素数は11としていますが、これはイメージとしては1+10であり、 1つのリッスンソケットと10個のサーバーソケットのステータスの変更を検出する狙いです。 初期状態では、サーバーソケットは作成されていませんからNULLを指定し、 リッスンソケットは配列の先頭に指定するようにしています。 eventsは、検出したいステータスを指定し、POLLRDNORMは接続または読み取りの検出を行います。

nResult = WSAPoll(fdArray, nMaxSocketCount, 500);
if (nResult < 0) {
	SendMessage(g_hwndListBox, LB_ADDSTRING, 0, (LPARAM)TEXT("WSAPOLLFDの実行に失敗しました。"));
	break;
}
else if (nResult == 0)
	continue;
else
	;

WSAPollが0以下の値を返した場合は関数が失敗したことを意味するため、その旨を表示するようにしています。 0が返った場合は、タイムアウトが発生したことを意味し、 この場合は後続の処理を実行せずにループするようにしています。 このWSAPollの呼び出しでは500ミリ秒というタイムアウト値を指定していますが、 本来ならば-1などの負の値を指定し、ステータスが変更されるまで待機するのが理想です。 しかし、そうした場合は、待機中にg_bExitThreadの値を確認することができず、 スレッドが終了すべきタイミングを特定できなくなりますから、 タイムアウトを指定して一定の間隔で制御を返す実装にしています。 0より大きい値が返された場合は、配列内のどのソケットにステータスの変更が生じたのかを次の処理で確認します。

for (i = 0; i < nMaxSocketCount; i++) {
	if (fdArray[i].revents & POLLRDNORM)
		break;
	else if (fdArray[i].revents & POLLHUP)
		break;
	else
		;
}

ソケットの現在のステータスは、reventsメンバに格納されています。 この値がPOLLRDNORMであるということは、接続または読み取りが発生していることを意味するため、 ループを抜けることになります。 POLLHUPは切断が行われたことを意味し、これも重要なステータスの変更ですからループを抜けることになります。

以降の処理については、以前に紹介したselectを呼び出すサーバープログラムの内容と同じ流れです。 リッスンソケットに対してPOLLRDNORMが格納されているならば、 接続ということですからacceptを呼び出し、 そうでないならばサーバーソケットにクライアントからのデータが届いていることになるため、 recvを呼び出して受信することになります。

Winsockとセキュリティ

Windows Vistaから登場したWinsock関数には、WSASetSocketSecurityという関数もあります。 この関数を呼び出せば、通信時にIPSecプロトコルを利用した暗号化を行えるようになるはずですが、 現在のWindowsではこの関数を呼び出すわけにはいきません。 WSASetSocketSecurityの呼び出し自体は成功するのですが、 その後に呼び出すことになるconnect、またはlistenの呼び出しで永久にブロッキングしてしまうからです。 さらに、どういうわけか、ブロッキングしたプロセスは強制終了することができないため、 システムのシャットダウンを妨げるという事態にまで至ります。 Windows SDKに含まれる下記のサンプルを実行しても上記のような問題が発生することから、 対策を考えるのは難しいといえます。

%Program Files%Microsoft SDKs\Windows\v6.0\Samples\NetDs\winsock\securesocket

WSASetSocketSecurityのリファレンスによれば、 この関数はconnectの前、もしくはbindの前に呼び出すべきとされています。 実際のところ、上記のサンプルではそのようになっているのですが、 やはりクライアントプログラムではconnectで永久にブロッキングしてしまい、 サーバープログラムではlistenで永久にブロッキングしてしまいます。



戻る