EternalWindows
WinHTTP / 非同期通信

WinHTTPのいくつかの関数では、関数が制御を返すまでの時間がそれなりに掛かることがあります。 これは関数が同期的に動作しているからであり、 通常このような処理が完了するまで制御を返さないという仕様は望ましいものです。 ただし、ウインドウを表示するアプリケーションなどで待ち時間が発生してしまっては、 その間にユーザー入力に応じることができなくなるため、 こうした場合はWinHTTPの関数を非同期に実行することが推奨されます。 非同期の設定は、WinHttpOpenの最終引数にWINHTTP_FLAG_ASYNCを指定することで可能です。

関数を非同期に実行するということは、処理が完了する前に関数が制御を返すことを意味します。 それでは、本来の処理はどこで行われているのかというと、これはWinHTTPの内部で作成されたスレッドになります。 アプリケーションがWinHttpSetStatusCallbackでコールバック関数を設定していれば、 WinHTTPのスレッドがコールバック関数を呼び出して処理の完了を通知してくれます。

WINHTTP_STATUS_CALLBACK WINAPI WinHttpSetStatusCallback(
  HINTERNET hInternet,
  WINHTTP_STATUS_CALLBACK lpfnInternetCallback,
  DWORD dwNotificationFlags,
  DWORD_PTR dwReserved
);

hInternetは、コールバック関数の第1引数に渡したいハンドルを指定します。 lpfnInternetCallbackは、コールバック関数のアドレスを指定します。 dwNotificationFlagsは、通知を受け取りたい種類を表す定数を指定します。 WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONSを指定すれば、あらゆる種類の通知を受け取ることになります。 dwReservedは、予約されているため0を指定します。

WinHttpSetStatusCallbackに指定するコールバック関数は、次のようなプロトタイプを持たなければなりません。

typedef void ( CALLBACK *WINHTTP_STATUS_CALLBACK )(
  HINTERNET hInternet,
  DWORD_PTR dwContext,
  DWORD dwInternetStatus,
  LPVOID lpvStatusInformation,
  DWORD dwStatusInformationLength
);

hInternetは、WinHttpSetStatusCallbackの第1引数が格納されます。 dwContextは、WinHttpSendRequestの最終引数に指定した値が格納されます。 dwInternetStatusは、通知の種類を表す定数が格納されます。 lpvStatusInformationは、通知に関する情報を格納したバッファへのアドレスが格納されます。 dwStatusInformationLengthは、lpvStatusInformationのサイズが格納されます。

今回のプログラムは、非同期通信を行う例を示します。 プログラムを実行するとメッセージボックスが表示されますが、これに応答せずにしばらく待っているとデータが表示されます。

#include <windows.h>
#include <winhttp.h>

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

struct REQUEST {
	BYTE buffer[4096];
};
typedef REQUEST *LPREQUEST;

void CALLBACK WinHttpStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HINTERNET      hSession, hConnect, hRequest;
	URL_COMPONENTS urlComponents;
	WCHAR          szHostName[256], szUrlPath[2048];
	WCHAR          szUrl[] = L"http://eternalwindows.jp/winbase/base/base00.html";
	LPREQUEST      lpRequest;

	hSession = WinHttpOpen(L"Sample Application/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
	if (hSession == NULL)
		return 0;

	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTS));
	urlComponents.dwStructSize     = sizeof(URL_COMPONENTS);
	urlComponents.lpszHostName     = szHostName;
	urlComponents.dwHostNameLength = sizeof(szHostName) / sizeof(WCHAR);
	urlComponents.lpszUrlPath      = szUrlPath;
	urlComponents.dwUrlPathLength  = sizeof(szUrlPath) / sizeof(WCHAR);

	if (!WinHttpCrackUrl(szUrl, lstrlenW(szUrl), 0, &urlComponents)) {
		WinHttpCloseHandle(hSession);
		return 0;
	}

	hConnect = WinHttpConnect(hSession, szHostName, INTERNET_DEFAULT_PORT, 0);
	if (hConnect == NULL) {
		WinHttpCloseHandle(hSession);
		return 0;
	}

	hRequest = WinHttpOpenRequest(hConnect, L"GET", szUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
	if (hRequest == NULL) {
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}
	
	WinHttpSetStatusCallback(hRequest, WinHttpStatusCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0);

	lpRequest = (LPREQUEST)HeapAlloc(GetProcessHeap(), 0, sizeof(REQUEST));
	if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, (DWORD)lpRequest)) {
		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}
	
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	
	HeapFree(GetProcessHeap(), 0, lpRequest);
	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);
	
	return 0;
}

void CALLBACK WinHttpStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
	HINTERNET hRequest = hInternet;
	LPREQUEST lpRequest = (LPREQUEST)dwContext;

	if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE)
		WinHttpReceiveResponse(hRequest, NULL);
	else if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE) {
		DWORD dwStatusCode = 0;
		DWORD dwSize = sizeof(DWORD);
		
		WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);
		if (dwStatusCode == HTTP_STATUS_OK)
			WinHttpReadData(hRequest, lpRequest->buffer, sizeof(lpRequest->buffer), NULL);
		else {
			TCHAR szBuf[256];
			wsprintf(szBuf, TEXT("Status Code %d"), dwStatusCode);
			MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		}
	}
	else if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_READ_COMPLETE)
		MessageBoxA(NULL, (LPSTR)lpRequest->buffer, "OK", MB_OK);
	else if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_REQUEST_ERROR) {
		LPWINHTTP_ASYNC_RESULT lpAsyncResult = (LPWINHTTP_ASYNC_RESULT)lpvStatusInformation;
		TCHAR                  szBuf[256];
		wsprintf(szBuf, TEXT("Error API %d, Error %d"), lpAsyncResult->dwResult, lpAsyncResult);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	}
	else
		;
}

非同期通信を行うために、WinHttpOpenの最終引数にWINHTTP_FLAG_ASYNCを指定するようにします。 また、WinHttpSetStatusCallbackを呼び出すことでコールバック関数の設定も行っています。 REQUEST構造体はコールバック関数に渡したいデータの型であり、 データを受け取るためのバッファを持っています。 この他に、データの取得対象となるURLなどを含んでおくのも面白いかもしれません。

WinHttpSetStatusCallbackの第1引数にはリクエストハンドルを指定していたため、 コールバック関数の第1引数はリクエストハンドルになります。 これは、WinHTTP関数の呼び出しに使用することができます。 コールバック関数に最初に贈られる通知は、WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETEです。 これはWinHttpSendRequestの処理が完了した際に通知され、 WinHttpReceiveResponseでサーバーからのレスポンスを取得することができます。 WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLEが通知された場合は、レスポンスを取得が完了したことを意味し、 WinHttpQueryHeadersでステータスコードの確認を行えます。 これが成功を表している場合は、WinHttpReadDataでデータを取得することができます。 データの取得が完了した場合はWINHTTP_CALLBACK_STATUS_READ_COMPLETEが通知されるため、 この際にデータを参照することができます。 WINHTTP_CALLBACK_STATUS_REQUEST_ERRORは、処理の最中にエラーが発生した場合に通知されます。 この場合はlpvStatusInformationをLPWINHTTP_ASYNC_RESULTで識別することができ、 dwResultからエラーが発生したAPIを、dwErrorからエラーの原因を取得することができます。 たとえば、dwResultがAPI_SEND_REQUESTならばWinHttpSendRequestの失敗を意味し、 dwErrorがERROR_WINHTTP_CANNOT_CONNECTならば、接続に関する問題であると分かります。


戻る