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

Winsockを使用してネットアプリケーションを開発する場合は、 どのような関数を呼び出してブロッキングの問題に対処するかを検討しておく必要があります。 たとえば、前節のサーバープログラムでは、selectを呼び出すことによってacceptとrecvにおけるブロックキングを回避し、 何らかのネットワークイベントが発生したら関数が制御を返すという実装に成功しました。 こうした実装は、WSAEventSelectとWSAWaitForMultipleEventsでも可能であり、 この方法ならばネットワークイベント以外に独自のイベントオブジェクトが変更された場合でも、 関数が制御を返すことができます。

上記の方法に共通しているのは、関数(selectまたはWSAWaitForMultipleEvents)でブロッキングが発生するという関係上、 独自のスレッドを作成してそこで関数を呼び出すという点ですが、 実際のところこれを好まない場合も多いと思われます。 単純に考えて、データの管理が複雑になるからです。 このため、アプリケーションがウインドウを持っているならば、 次に示すWSAAsyncSelectを呼び出す実装にしたほうがよいと思われます。 この関数は、メッセージを送信することによってイベントの発生を通知するため、 ブロッキングについて意識する必要がなくなります。

int WSAAsyncSelect(
  SOCKET s,
  HWND hWnd,
  unsigned int wMsg,
  long lEvent
);

sは、ソケットの記述子を指定します。 hWndは、メッセージを受け取るウインドウハンドルを指定します。 wMsgは、ネットワークイベントが発生した場合に送信するメッセージを指定します。 lEventは、通知を希望するイベントの定数を指定します。 次に、定義されているイベントの一部を示します。

定数 説明
FD_READ データが届いていることを通知する。
FD_ACCEPT 接続要求が届いていることを通知する。
FD_CONNECT 接続が完了したことを通知する。
FD_CLOSE 相手が切断したことを通知する。

ソケットが指定したイベントの状態になった場合は、 第3引数のメッセージが第2引数のウインドウに送られます。 このとき、どのイベントが発生したのかは、 lParamをWSAGETSELECTEVENTマクロに指定することで分かります。 wParamはイベントが発生したソケットの識別子が格納されているため、 ソケットを複数作成している場合でも、 どのソケットに対するメッセージなのかを特定できることになります。

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

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

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

#define WM_SOCKET WM_APP

SOCKET InitializeWinsock(LPSTR lpszPort);
int GetSocketIndex(SOCKET soc, SOCKET socServer[], int nMaxSocketCount);
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 int    nMaxSocketCount = 10;
	static SOCKET socListen = INVALID_SOCKET;
	static SOCKET socServer[10];
	static HWND   hwndListBox = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		int i;

		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("5000");
		if (socListen == INVALID_SOCKET)
			return -1;

		for (i = 0; i < nMaxSocketCount; i++)
			socServer[i] = INVALID_SOCKET;

		WSAAsyncSelect(socListen, hwnd, WM_SOCKET, FD_ACCEPT);

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

		case FD_ACCEPT: {
			int              i;
			int              nAddrLen;
			char             szBuf[256];
			char             szHostName[256];
			SOCKADDR_STORAGE sockAddr;

			i = GetSocketIndex(INVALID_SOCKET, socServer, nMaxSocketCount);
			
			nAddrLen = sizeof(SOCKADDR_STORAGE);
			socServer[i] = accept(socListen, (LPSOCKADDR)&sockAddr, &nAddrLen);
			WSAAsyncSelect(socServer[i], hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
			
			getnameinfo((LPSOCKADDR)&sockAddr, nAddrLen, szHostName, sizeof(szHostName), NULL, 0, 0);
			wsprintfA(szBuf, "No%d(%s) 接続", i + 1, szHostName);
			SendMessageA(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

			break;
		}

		case FD_READ: {
			int   i;
			int   nLen;
			int   nResult;
			TCHAR szBuf[256];
			TCHAR szData[256];

			i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

			nLen = sizeof(szData);
			nResult = recv(socServer[i], (char *)szData, nLen, 0);
			
			wsprintf(szBuf, TEXT("No%d %s"), i + 1, szData);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
					
			nLen = nResult;
			nResult = send(socServer[i], (char *)szData, nLen, 0);

			break;
		}

		case FD_CLOSE: {
			int   i;
			TCHAR szBuf[256];

			i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

			wsprintf(szBuf, TEXT("No%d 切断"), i + 1);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
			
			shutdown(socServer[i], SD_BOTH);
			closesocket(socServer[i]);
			socServer[i] = INVALID_SOCKET;

			break;
		}

		}
		return 0;

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

	case WM_DESTROY: {
		int i;

		for (i = 0; i < nMaxSocketCount; i++) {
			if (socServer[i] != INVALID_SOCKET) {
				shutdown(socServer[i], SD_BOTH);
				closesocket(socServer[i]);
			}
		}
		
		if (socListen != INVALID_SOCKET) {
			closesocket(socListen);
			WSACleanup();
		}

		PostQuitMessage(0);
		return 0;
	}

	default:
		break;

	}

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

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;
}

int GetSocketIndex(SOCKET soc, SOCKET socServer[], int nMaxSocketCount)
{
	int i;

	for (i = 0; i < nMaxSocketCount; i++) {
		if (socServer[i] == soc)
			break;
	}

	return i;
}

WM_CREATEで呼び出しているWSAAsyncSelectでは、 リッスンソケットにFD_ACCEPTイベントを関連付けています。 これにより、クライアントが接続してくると、 lParamにFD_ACCEPTが格納されたWM_SOCKETが送られることになります。 WM_SOCKETは独自に定義したメッセージであり、WM_APPの値を基にしています。 FD_ACCEPTの処理は、次のようになっています。

case FD_ACCEPT: {
	int              i;
	int              nAddrLen;
	char             szBuf[256];
	char             szHostName[256];
	SOCKADDR_STORAGE sockAddr;

	i = GetSocketIndex(INVALID_SOCKET, socServer, nMaxSocketCount);
	
	nAddrLen = sizeof(SOCKADDR_STORAGE);
	socServer[i] = accept(socListen, (LPSOCKADDR)&sockAddr, &nAddrLen);
	WSAAsyncSelect(socServer[i], hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
	
	getnameinfo((LPSOCKADDR)&sockAddr, nAddrLen, szHostName, sizeof(szHostName), NULL, 0, 0);
	wsprintfA(szBuf, "No%d(%s) 接続", i + 1, szHostName);
	SendMessageA(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

	break;
}

クライアントからの接続要求が届いているため、acceptを呼び出してクライアントと接続されたサーバーソケットを取得します。 socServerはサーバーソケットの配列であり、socServer[i]が未接続のサーバーソケットを表すように、 GetSocketIndexで適切なインデックスを取得します。 第1引数にINVALID_SOCKETを指定しているため、 socServerの中でINVALID_SOCKETである要素のインデックスが返ることになります。 取得したサーバーソケットに対して、WSAAsyncSelectを呼び出す点は非常に重要です。 これにより、読み取りまたは切断の際にメッセージを受信することができるようになります。 FD_READの処理は、次のようになっています。

case FD_READ: {
	int   i;
	int   nLen;
	int   nResult;
	TCHAR szBuf[256];
	TCHAR szData[256];

	i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

	nLen = sizeof(szData);
	nResult = recv(socServer[i], (char *)szData, nLen, 0);
	
	wsprintf(szBuf, TEXT("No%d %s"), i + 1, szData);
	SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
			
	nLen = nResult;
	nResult = send(socServer[i], (char *)szData, nLen, 0);

	break;
}

WSAAsyncSelectで送られるメッセージのwParamは、イベントが発生したソケットになっています。 これをGetSocketIndexに指定することにより、socServerの中のどのソケットにイベントが発生したかが分かります。 データが届いているためrecvでデータを受信し、 受信できたことをクライアントに伝えるためにsendを呼び出しています。 本来ならば、応答として適切なデータを送信したいところですが、 今回は簡単のため受信データを送信するようにしています。 FD_CLOSEの処理は、次のようになっています。

case FD_CLOSE: {
	int   i;
	TCHAR szBuf[256];

	i = GetSocketIndex((SOCKET)wParam, socServer, nMaxSocketCount);

	wsprintf(szBuf, TEXT("No%d 切断"), i + 1);
	SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
	
	shutdown(socServer[i], SD_BOTH);
	closesocket(socServer[i]);
	socServer[i] = INVALID_SOCKET;

	break;
}

FD_CLOSEが送られた場合は、クライアントが接続を切断したことを意味しますから、 サーバーソケットも開放することになります。 まず、shutdownでサーバーソケットを無効にし、その後にclosesocketで閉じるようにします。 閉じられたサーバーソケットには、未接続であることを示すためにINVALID_SOCKETを格納しています。

WSAAsyncGetHostByNameについて

WSAAsyncSelectのように、非同期の仕組みとしてメッセージを採用している関数にはWSAAsyncGetHostByNameがあります。 この関数はいわば、getaddrinfo(及び従来のgethostbyname)を非同期に実装したものであり、 ホスト名からIPアドレスの取得するまで処理をブロッキングするようなことはありません。 関数自体は直ちに制御を返し、IPアドレスを取得できた時点でウインドウにメッセージが送られる仕組みになっています。 次に、WSAAsyncGetHostByNameの使用例を示します。

#include <winsock2.h>

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

#define ID_SEND 100
#define ID_EDIT 200
#define WM_HOST WM_APP

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");
	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 hAsync = NULL;
	static HWND   hwndButton = NULL;
	static HWND   hwndEditBox = NULL;
	static BOOL   bSearching = FALSE;
	static BYTE   hostBuf[MAXGETHOSTSTRUCT];

	switch (uMsg) {

	case WM_CREATE: {
		WSADATA wsaData;
		
		hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("検索"), WS_CHILD | WS_VISIBLE, 10, 10, 110, 30, hwnd, (HMENU)ID_SEND, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		hwndEditBox = CreateWindowEx(0, TEXT("EDIT"), TEXT("ホスト名を入力してください。"), WS_CHILD | WS_VISIBLE | WS_BORDER, 140, 10, 300, 35, hwnd, (HMENU)ID_EDIT, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		WSAStartup(MAKEWORD(2, 2), &wsaData);

		return 0;
	}
	
	case WM_COMMAND: {
		char szData[256];

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

		if (bSearching) {
			WSACancelAsyncRequest(hAsync);

			bSearching = FALSE;
			SetWindowText(hwndButton, TEXT("検索"));
		}
		else {
			GetWindowTextA(hwndEditBox, szData, sizeof(szData));
			hAsync = WSAAsyncGetHostByName(hwnd, WM_HOST, szData, (char *)hostBuf, sizeof(hostBuf));

			bSearching = TRUE;
			SetWindowText(hwndButton, TEXT("キャンセル"));
		}

		return 0;
	}

	case WM_HOST: {
		LPHOSTENT lpHostnet;
		IN_ADDR   addr;
		LPSTR     lpszIPAddress;

		bSearching = FALSE;
		SetWindowText(hwndButton, TEXT("検索"));

		if (WSAGETASYNCERROR(lParam) != 0) {
			MessageBox(hwnd, TEXT("IPアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}

		lpHostnet = (LPHOSTENT)hostBuf;
		addr.S_un.S_addr = *((PULONG)lpHostnet->h_addr_list[0]);
		lpszIPAddress = inet_ntoa(addr);

		SetWindowTextA(hwndEditBox, lpszIPAddress);

		return 0;
	}

	case WM_DESTROY:
		WSACleanup();
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

このプログラムは、エディットボックスに入力されたホスト名から対応するIPアドレスを取得します。 この取得をWSAAsyncGetHostByNameで行うことによって、 ブロッキング中にウインドウ操作ができなくなる問題を回避しています。 「検索」ボタンが押された場合はWM_COMMANDが送られ、 検索中であることを示すbSearchingがTRUEかどうかで、行われる処理が変化します。 まずは、FALSEである場合の処理から注目します。 この場合は、これからIPアドレスの検索を行うことになるため、 GetWindowTextで入力されたホスト名を取得し、これをWSAAsyncGetHostByNameの第3引数に指定します。 第2引数はIPアドレスの取得時や失敗時にウインドウへ送るメッセージであり、 ここでは独自に定義したWM_HOSTというメッセージを指定します。 第4引数は、同メッセージが送られた際に、IPアドレス情報を格納しておくことを望むバッファを指定します。 メッセージ内では、このバッファを参照することでIPアドレスを取得することになります。 bSearchingがTRUEである場合に呼び出しているWSACancelAsyncRequestは、 現在実行している非同期操作をキャンセルします。 このキャンセルには、実行している非同期操作を示すハンドルが必要になるため、 WSAAsyncGetHostByNameの戻り値を指定するようにしています。 これにより、IPアドレスの検索をキャンセルできるようになります。

IPアドレスの検索が終了した場合は、WSAAsyncGetHostByNameの第2引数に指定したメッセージが送られることになります。 まず、WSAGETASYNCERRORマクロにlParamを指定し、これが0でないかを確認します。 0でない場合は、ホスト名に対応するIPアドレスを検索できなかったことを意味するため、検索に失敗した旨を表示します。 0である場合は、IPアドレスを取得できたことを意味するため、 WSAAsyncGetHostByNameの第4引数に指定したバッファが初期化されていることになります。 まず、バッファをHOSTENT構造体として表し、 h_addr_listから数値化されたIPアドレスを参照します。 そして、この値をIN_ADDR構造体のS_un.S_addrに指定してinet_ntoaを呼び出せば、 文字列化されたIPアドレスを取得することができます。 inet_ntoaは、IPアドレスをIPv4形式でのみ返す関数ですが、 WSAAsyncGetHostByName自体がIPv4のみに対応しているため問題はありません。



戻る