EternalWindows
WinHTTP / GETによる取得

ネットワーク通信で使用されるプロトコルには様々な種類が存在しますが、 その中でもHTTPほどユーザーにとって身近なプロトコルはないといえるでしょう。 ユーザーがブラウザなどでWebページを閲覧するためには、そのページの基なるHTMLファイルをサーバーに要求しなければなりませんが、 ここで使用されるプロトコルがHTTPです。 ブラウザのアドレスバーにURLが入力されたり、何らかのURLへのリンクがクリックされたりした場合、 ブラウザはURLのhttp:の部分からHTTP通信の必要を理解し、 URLからサーバーのホスト名を把握して実際にサーバーへ接続します。 そしてこれが完了したら、HTTPプロトコルに従った形式でサーバーと通信を行います。 WinHTTPというAPIは、このHTTPプロトコルの詳細をアプリケーションから隠蔽するため、 接続や送信などの関数を呼び出すだけで、サーバーとのHTTP通信を実現できます。

HTTP通信を簡略化するAPIは、WinHTTPの他にWinINetが存在します。 WinINetはHTTP通信の他にFTP通信やキャッシュ操作などをサポートし、 UIを表示するブラウザのようなアプリケーション向けに設計されているといわれています。 これに対してWinHTTPは、UIを表示しないサービスアプリケーション向けに設計されているといわれています。 ただし、WinHTTPはWinINetよりも安全かつ堅牢であるとされているため、 基本的にHTTP通信はWinHTTPで行うのがよいと思われます。

アプリケーションがWinHTTPを使用するためには、WinHttpOpenでセッションハンドルを取得することになります。 この関数は、原則として一度だけ呼び出します。

HINTERNET WINAPI WinHttpOpen(
  LPCWSTR pwszUserAgent,
  DWORD dwAccessType,
  LPCWSTR pwszProxyName,
  LPCWSTR pwszProxyBypass,
  DWORD dwFlags
);

pwszUserAgentは、ユーザーエージェントを表す文字列を指定します。 この文字列はHTTPリクエストのヘッダに含まれることになり、 サーバーはこの文字列からアクセスしてきたアプリケーションを判定できます。 dwAccessTypeは、プロキシを使用するかどうかに関する定数を指定します。 プロキシを使用しない場合は、WINHTTP_ACCESS_TYPE_NO_PROXYを指定します。 pwszProxyNameは、使用するプロキシのサーバー名を指定します。 プロキシを使用しない場合は、WINHTTP_NO_PROXY_NAMEを指定します。 pwszProxyBypassは、プロキシを通じてアクセスしたくないサーバーのリストを指定します。 第3引数にWINHTTP_NO_PROXY_NAMEを指定した場合は、WINHTTP_NO_PROXY_BYPASSを指定します。 dwFlagsは、非同期通信に関する定数を指定します。 WINHTTP_FLAG_ASYNCを指定した場合は非同期通信になります。

WinHttpOpenでセッションハンドルを取得したら、WinHttpConnectでサーバーに接続することができます。 この関数は、通信したいサーバーの数だけ呼び出すことになります。

HINTERNET WINAPI WinHttpConnect(
  HINTERNET hSession,
  LPCWSTR pswzServerName,
  INTERNET_PORT nServerPort,
  DWORD dwReserved
);

hSessionは、WinHttpOpenで取得したセッションハンドルを指定します。 pswzServerNameは、接続するホスト名を指定します。 nServerPortは、サーバーのポート番号を指定します。 通常のHTTP通信の場合はINTERNET_DEFAULT_HTTP_PORTを指定し、SSL通信の場合はINTERNET_DEFAULT_HTTPS_PORTを指定します。 INTERNET_DEFAULT_PORTを指定すれば、適切なポートが関数側で判断されます。 dwReservedは、予約されているため0を指定します。

サーバーに要求を送るためには、WinHttpOpenRequestでリクエストハンドルを作成しなければなりません。 この関数は、要求したいデータの数だけ(いわばアクセスしたいページの数だけ)呼び出すことになります。

HINTERNET WINAPI WinHttpOpenRequest(
  HINTERNET hConnect,
  LPCWSTR pwszVerb,
  LPCWSTR pwszObjectName,
  LPCWSTR pwszVersion,
  LPCWSTR pwszReferrer,
  LPCWSTR *ppwszAcceptTypes,
  DWORD dwFlags
);

hConnectは、WinHttpConnectで取得した接続ハンドルを指定します。 pwszVerbは、"GET"や"POST"のようなHTTP動詞を指定します。 NULLを指定した場合は、"GET"を使用すると解釈されます。 pwszObjectNameは、HTTP要求の対象となる名前を指定します。 通常は、URL中のホスト名以降の部分を指定します。 pwszVersionは、HTTPのバージョンを指定します。 NULLを指定した場合は、バージョンが1.1であると見なされます。 pwszReferrerは、参照元URLを指定します。 通常は、参照元URLがないことを示すWINHTTP_NO_REFERERを指定します。 ppwszAcceptTypesは、クライアントが受け入れることのできるメディアの種類を指定します。 テキストしか受け入れない場合は、WINHTTP_DEFAULT_ACCEPT_TYPESを指定します。 dwFlagsは、様々な定数を指定することができます。 これは必要になった際に説明します。

リクエストハンドルを作成したら、WinHttpSendRequestでリクエストをサーバーに送信できます。 つまり、HTTPリクエストを実際に発行します。

BOOL WINAPI WinHttpSendRequest(
  HINTERNET hRequest,
  LPCWSTR pwszHeaders,
  DWORD dwHeadersLength,
  LPVOID lpOptional,
  DWORD dwOptionalLength,
  DWORD dwTotalLength,
  DWORD_PTR dwContext
);

hRequestは、WinHttpOpenRequestで取得したリクエストハンドルを指定します。 pwszHeadersは、HTTPヘッダに追加したい文字列(ヘッダ)を指定します。 明示的に追加したいヘッダがない場合は、WINHTTP_NO_ADDITIONAL_HEADERSを指定します。 dwHeadersLengthは、pwszHeadersの長さを指定します。 lpOptionalは、要求のボディとして含めたいデータを格納したバッファを指定します。 GETメソッドの場合はWINHTTP_NO_REQUEST_DATA(NULL)になりますが、POSTメソッドの場合はバッファを指定することがあります。 dwTotalLengthは、HTTPリクエストボディに含めるデータの総サイズを指定します。 dwContextは、コールバック関数が設定されている際に、そのコールバック関数へ渡したいデータを指定します。

サーバーにリクエストを送信したら、サーバーからのレスポンスをWinHttpReceiveResponseで受信できます。 受信したレスポンスは、WinHTTPによって管理されます。

BOOL WINAPI WinHttpReceiveResponse(
  HINTERNET hRequest,
  LPVOID lpReserved
);

hRequestは、リクエストハンドルを指定します。 lpReservedは、予約されているためNULLを指定します。

受信したレスポンスには、ヘッダとボディ(任意)が含まれています。 ヘッダを取得するには、WinHttpQueryHeadersを呼び出します。

BOOL WINAPI WinHttpQueryHeaders(
  HINTERNET hRequest,
  DWORD dwInfoLevel,
  LPCWSTR pwszName,
  LPVOID lpBuffer,
  LPDWORD lpdwBufferLength,
  LPDWORD lpdwIndex
);

hRequestは、リクエストハンドルを指定します。 dwInfoLevelは、ヘッダに含まれる情報をどのように取得するかを示す定数を指定します。 pwszNameは、情報を取得したいヘッダの名前を指定します。 WINHTTP_HEADER_NAME_BY_INDEXを指定した場合は、対象となるヘッダをdwInfoLevelに指定済みであることを意味します。 lpBufferは、ヘッダの情報を受け取るバッファを指定します。 lpdwBufferLengthは、バッファのサイズを格納した変数のアドレスを指定します。 lpdwIndexは、ヘッダのインデックスを格納したバッファを指定します。 これは、同一名のヘッダがある場合において、どのヘッダから情報を取得するのかを決定するために使用されます。 WINHTTP_NO_HEADER_INDEXを指定した場合は、最初のヘッダが対象になります。

レスポンスのボディからデータを取得するには、WinHttpReadDataを呼び出します。

BOOL WINAPI WinHttpReadData(
  HINTERNET hRequest,
  LPVOID lpBuffer,
  DWORD dwNumberOfBytesToRead,
  LPDWORD lpdwNumberOfBytesRead
);

hRequestは、リクエストハンドルを指定します。 lpBufferは、データを受け取るバッファを指定します。 dwNumberOfBytesToReadは、データを何バイト読み取るかを指定します。 lpdwNumberOfBytesReadは、読み取ったバイト数を受け取る変数のアドレスを指定します。

WinHTTPで使用されるセッションハンドル、接続ハンドル、リクエストハンドルは、 いずれもHINTERNETで識別されています。 そして、いずれのハンドルもWinHttpCloseHandleで閉じることができます。

BOOL WINAPI WinHttpCloseHandle(
  HINTERNET hInternet
);

hInternetは、閉じたいハンドルを指定します。

今回のプログラムは、サーバーに存在するファイルの中身をHTTPプロトコルで受信します。 WinHTTPの機能を使用する関係上、winhttp.hのインクルードとwinhttp.libへのリンクをしています。

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

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

LPBYTE ReadData(HINTERNET hRequest, LPDWORD lpdwSize);

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";
	LPBYTE         lpHeader, lpData;
	DWORD          dwSize;

	hSession = WinHttpOpen(L"Sample Application/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	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;
	}
	
	if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0)) {
		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);
		return 0;
	}

	WinHttpReceiveResponse(hRequest, NULL);

	dwSize = 0;
	WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX);
	lpHeader = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
	WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpHeader, &dwSize, WINHTTP_NO_HEADER_INDEX);
	MessageBoxW(NULL, (LPWSTR)lpHeader, L"ヘッダ", MB_OK);
	HeapFree(GetProcessHeap(), 0, lpHeader);

	lpData = ReadData(hRequest, &dwSize);
	MessageBoxA(NULL, (LPSTR)lpData, "ボディ", MB_OK);
	HeapFree(GetProcessHeap(), 0, lpData);
	
	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);
	
	return 0;
}

LPBYTE ReadData(HINTERNET hRequest, LPDWORD lpdwSize)
{
	LPBYTE lpData = NULL;
	LPBYTE lpPrev = NULL;
	DWORD  dwSize;
	DWORD  dwTotalSize = 0;
	DWORD  dwTotalSizePrev = 0;

	for (;;) {
		WinHttpQueryDataAvailable(hRequest, &dwSize);
		if (dwSize > 0) {
			dwTotalSizePrev = dwTotalSize;
			dwTotalSize += dwSize;
			lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwTotalSize);
			if (lpPrev != NULL) {
				CopyMemory(lpData, lpPrev, dwTotalSizePrev);
				HeapFree(GetProcessHeap(), 0, lpPrev);
			}
			WinHttpReadData(hRequest, lpData + dwTotalSizePrev, dwSize, NULL);
			lpPrev = lpData;
		}
		else
			break;
	}

	*lpdwSize= dwTotalSize;

	return lpData;
}

WinHTTPを使用するためには、最初にWinHttpOpenを呼び出すことになります。 第1引数の名前は適当な文字列でよく、第2引数から第4引数はプロキシを使用しない場合は上記のようになります。 第5引数は、非同期通信をしない場合は0で構いません。 WinHttpOpenが成功したら次にWinHttpConnectを呼び出したいところですが、 その前にWinHttpCrackUrlを呼び出しています。

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

WinHttpCrackUrlは第1引数のURLを分解して第4引数に返します。 こうした処理が必要になるのは、WinHttpConnectがURLではなくホスト名を受け取るからであり、 URLからホスト名の部分を取得する必要があるからです。 urlComponents.lpszUrlPathにはホスト名以下の部分が格納されますが、 これはWinHttpOpenRequestで必要になるため、これについても受け取るようにします。 ちなみに、各バッファのサイズは256と2048になっていますが、 これはwininet.hに定義されているINTERNET_MAX_HOST_NAME_LENGTHとINTERNET_MAX_PATH_LENGTHを参考にしています。

WinHttpConnectでサーバーに接続したら、サーバーにリクエストを送信することになります。 このためには、WinHttpOpenRequestでリクエストハンドルを作成し、 WinHttpSendRequestでリクエストを送信します。

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

if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0)) {
	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);
	return 0;
}

単純にHTMLファイルの中身を取得する場合は、WinHttpOpenRequestの第2引数は"GET"で構いません。 第3引数は取得対象となるファイルのパスを指定し、第4引数はHTTPのバージョン(NULLは1.1)を指定します。 第5引数は参照元URLを指定できますが、WINHTTP_NO_REFERERで問題ありません。 第6引数は受け取ることのできるメディアの種類であり、 テキスト形式を受け取る場合はWINHTTP_DEFAULT_ACCEPT_TYPESで問題ありません。 最終引数のフラグについては0で問題ありません。 WinHttpSendRequestは実際にリクエストを送信することができますが、 この際には任意のヘッダとボディを追加することができます。 しかし、今回はこれを行わないということで、 第2引数にはヘッダが不要なことを示すWINHTTP_NO_ADDITIONAL_HEADERSを、 第4引数にはボディが不要なことを示すWINHTTP_NO_REQUEST_DATAを指定しています。

WinHttpReceiveResponseを呼び出してサーバーのレスポンスを受信したら、 WinHttpQueryHeadersでヘッダを取得することができます。

dwSize = 0;
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX);
lpHeader = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpHeader, &dwSize, WINHTTP_NO_HEADER_INDEX);
MessageBoxW(NULL, (LPWSTR)lpHeader, L"ヘッダ", MB_OK);
HeapFree(GetProcessHeap(), 0, lpHeader);

第2引数にはどのヘッダの情報を取得するかを指定できますが、WINHTTP_QUERY_RAW_HEADERS_CRLFを指定すれば全ての情報を取得できます。 第2引数には情報を受け取るバッファを指定しますが、 この時点では必要なサイズが分からないためNULLを指定します。 第5引数で必要なサイズを取得し、2回目の呼び出しで実際にデータを取得します。

WINHTTP_QUERY_RAW_HEADERS_CRLFの結果に"HTTP/1.1 200 OK"が含まれている場合は、リクエストしたファイルが存在することを意味します。 つまり、ボディとしてファイルのデータが含まれています。 この場合は、WinHttpReadDataでそのデータを取得することができますが、 データを受け取るバッファはどのように用意すればよいでしょうか。 実はWinHttpQueryDataAvailableを呼び出すと、データのサイズを取得することができるため、 これを利用すれば次のようなコードが成立するように思えます。

WinHttpQueryDataAvailable(hRequest, &dwSize);
if (dwSize > 0) {
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
	WinHttpReadData(hRequest, lpData, dwSize, NULL);
	MessageBoxA(NULL, (LPSTR)lpData, "ボディ", MB_OK);
	HeapFree(GetProcessHeap(), 0, lpData);
}

このコードはWinHttpQueryDataAvailableでデータのサイズを取得し、 そのサイズ分だけバッファを確保してからデータを読み取っているわけですが、 実際にはこれだけの実装では不十分です。 なぜなら、WinHttpQueryDataAvailableは、ボディに含まれるデータの全サイズを返すようになっていないからです。 このため、WinHttpReadDataを呼び出す場合は、予め大きなサイズのバッファを用意しておくか、 少しずつデータを取得していくかのどちらかになります。 後者の場合は次のようになります。

LPBYTE ReadData(HINTERNET hRequest, LPDWORD lpdwSize)
{
	LPBYTE lpData = NULL;
	LPBYTE lpPrev = NULL;
	DWORD  dwSize;
	DWORD  dwTotalSize = 0;
	DWORD  dwTotalSizePrev = 0;

	for (;;) {
		WinHttpQueryDataAvailable(hRequest, &dwSize);
		if (dwSize > 0) {
			dwTotalSizePrev = dwTotalSize;
			dwTotalSize += dwSize;
			lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwTotalSize);
			if (lpPrev != NULL) {
				CopyMemory(lpData, lpPrev, dwTotalSizePrev);
				HeapFree(GetProcessHeap(), 0, lpPrev);
			}
			WinHttpReadData(hRequest, lpData + dwTotalSizePrev, dwSize, NULL);
			lpPrev = lpData;
		}
		else
			break;
	}

	*lpdwSize= dwTotalSize;

	return lpData;
}

複数回WinHttpReadDataを呼び出すという関係上、1回目に取得したデータと2回目に取得したデータをいかに連結するかが鍵となります。 lpDataは全てのデータを含むメモリであり、 lpPrev(前に取得したデータ)がある場合はまずこれをlpDataにコピーします。 この時点で、lpPrevは不要になるため開放するようにします。 WinHttpReadDataを呼び出す際には、前のデータを上書きしないようlpData + dwTotalSizePrevで調整しています。

WinHTTPとCOM

WinHTTPには今回使用した関数スタイルのAPIの他に、COMインターフェース用のAPIも存在しています。 このAPIを使用する場合は、まずWindows SDKのフォルダからhttprequest.idlを取り出し、 これをプロジェクトに追加してコンパイルします。 そうすると、httprequest_h.hという名前のヘッダーファイルが作成されますから、これをインクルードします。 これにより、IWinHttpRequestというインターフェースを使用できます。 コード例については、次のURLが参考になります。

http://msdn.microsoft.com/en-us/library/aa383989(VS.85).aspx

上記URLにはスクリプト言語からを使用する例も示されていますが、 このような事が可能なのはWinHTTPのオブジェクトがオートメーションをサポートしているからです。 つまり、IWinHttpRequestがIDispatchを継承するようになっています。



戻る