WinHTTP APIを用いたWebアクセスの概要

WinHTTP APIを用いたWebアクセスに関する関数の説明やサンプルコード、その他使えるテクニックを記載しています。

# オーバービュー
HTTP サーバーと対話するときに WinHTTP 関数が通常呼び出される順序を示しています。
網かけのボックスは HINTERNET ハンドルを生成する関数を表し、プレーン ボックスはそれらのハンドルを使用する関数を表します。

![](https://learn.microsoft.com/ja-jp/windows/win32/winhttp/images/art-winhttp3.png)

# 関数解説
必要最低限な関数について解説を記載します。  
なお、流れは以下の通りです。

1. WinHTTPの初期化（`WinHttpOpen`）
2. サーバーへの接続を準備（`WinHttpConnect`）
3. HTTPリクエストの作成（`WinHttpOpenRequest`）
4. HTTPリクエストを送信（`WinHttpSendRequest`）
5. サーバーからのレスポンスを受信（`WinHttpReceiveResponse`）
6. 受信したデータサイズを取得（`WinHttpQueryDataAvailable`）


## ライブラリやヘッダの利用
WinHTTPはワイド文字（Unicode）のみサポートしているため注意が必要です。

```cpp
#include <winhttp.h>
#pragma comment (lib, "winhttp.lib")
```

## WinHTTPの初期化
`WinHttpOpen`関数を用いてWinHTTPの初期化を行います。  
失敗した場合は`NULL`が返ってきます。

```cpp
// WinHTTPオブジェクトの作成
HINTERNET hSession = WinHttpOpen(
    L"UserAgent/WinHTTP 1.0",
    WINHTTP_ACCESS_TYPE_NO_PROXY,  // プロキシなしですべてのホスト名を直接解決する
    WINHTTP_NO_PROXY_NAME,         // winhttp.hではdefineディレクティブでNULLがトークンとして定義されている
    WINHTTP_NO_PROXY_BYPASS,       // winhttp.hではdefineディレクティブでNULLがトークンとして定義されている
    0);
```

> [WinHttpOpen 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpopen)

## サーバーへの接続を準備
`WinHttpConnect`関数を用いてHTTPサーバーへ接続するための準備を行います。  
失敗した場合は`NULL`が返ってきます。

```cpp
// サーバに接続するための準備
HINTERNET hConnect = WinHttpConnect(
    hSession,                      // WinHttpOpenで作成したセッション
    L"www.example.com",            // サーバーのホスト部を指定する
    INTERNET_DEFAULT_HTTPS_PORT,   // ポート番号を指定する, 一部は定義されている
    0);
```

> [WinHttpConnect 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpconnect)

## HTTPリクエストの作成
`WinHttpOpenRequest `関数を用いてHTTPリクエストのハンドルを作成します。  
失敗した場合は`NULL`が返ってきます。

```cpp
// HTTPリクエストの作成
HINTERNET hRequest = WinHttpOpenRequest(
    hConnect,                       // WinHttpConnectで作成したハンドル
    L"GET",                         // リクエストメソッド
    L"/index.html",                 // URLのパス部
    NULL,                           // HTTPのバージョン（NULLの場合はHTTP/1.1になる）
    WINHTTP_NO_REFERER,             // リファラー
    WINHTTP_DEFAULT_ACCEPT_TYPES,   // Acceptヘッダー
    WINHTTP_FLAG_SECURE);           // HTTPS接続を行う場合は設定
```

> [WinHttpOpenRequest 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpopenrequest)

## HTTPリクエストを送信
`WinHttpSendRequest`関数を用いてHTTPリクエストを送信します。  
失敗した場合は`FALSE`が返ってきます。

```cpp
// HTTPリクエストを送信
BOOL bSent = WinHttpSendRequest(
    hRequest,                       // WinHttpOpenRequestで作成したハンドル
    WINHTTP_NO_ADDITIONAL_HEADERS,  // リクエストに追加するヘッダー
    0,                              // リクエストに追加するヘッダーの長さ（文字数）
    WINHTTP_NO_REQUEST_DATA,        // リクエストヘッダー直後に送信するデータへのポインタ（POST, PUTの時のみ）
    0,                              // リクエストヘッダー直後に送信するデータのサイズ（バイト単位）
    0,                              // 送信された合計データの長さ（バイト単位）、0で自動計算される
    0);                             // コールバック関数に渡されるアプリケーション定義
```
> [WinHttpSendRequest 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpsendrequest)

## サーバーからのレスポンスを受信
`WinHttpReceiveResponse`関数を用いてHTTPリクエストに対するレスポンスを受信します。  
失敗した場合は`FALSE`が返ってきます。

なお`WinHttpReceiveResponse`はブロッキング動作を行うためデータが届くまで待機します。
```cpp
BOOL bReceived = WinHttpReceiveResponse(
    hRequest,   // WinHttpOpenRequestで作成したハンドル
    NULL);      // NULLを指定する必要あり（パラメーター自体が予約値）
```

> [WinHttpReceiveResponse 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpreceiveresponse)

## 受信したデータサイズを取得
`WinHttpQueryDataAvailable`関数を用いて受信したレスポンスの読み取ることができるデータのサイズをバイト単位で取得します。  
失敗した場合は`FALSE`が返ってきます。また、データサイズは第二引数の変数に格納されます。

```cpp
DWORD dwSize = 0;
BOOL bAvailable = WinHttpQueryDataAvailable(
    hRequest,   // WinHttpOpenRequestで作成したハンドル
    &dwSize);   // バイト数を受け取る変数へのポインタ
```
> [WinHttpQueryDataAvailable 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpquerydataavailable)

## 受信したデータを読み取る
`WinHttpReadData`関数を用いてHTTPレスポンスのデータを読み取ります。  
失敗した場合は`FALSE`が返ってきます。

```cpp
// WinHttpQueryDataAvailable関数で算出したバイト数+NULL終端文字分のサイズの配列を作成
char* buffer = new char[dwSize + 1];
SecureZeroMemory(buffer, dwSize + 1);

DWORD BytesRead = 0;
BOOL bReadData = WinHttpReadData(
    hRequest,       // WinHttpOpenRequestで作成したハンドル
    buffer.data(),  // 読み取ったデータの格納先バッファーのポインタ、 vectorで定義した場合は.data()で先頭アドレスを返す
    dwSize,         // 読み取るデータサイズ（バイト単位）
    &BytesRead);    // 読み取られたデータサイズ（バイト単位）
```

> [WinHttpReadData 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpreaddata)

# その他テクニック
## プロキシ情報を取得して利用する
`WinHttpGetIEProxyConfigForCurrentUser`関数を用いて`WINHTTP_CURRENT_USER_IE_PROXY_CONFIG`型構造体に取得したデータを格納します。  
その後三項演算子を用いて`proxyConfig.lpszProxy`メンバーが`NULL`でなければ`WINHTTP_ACCESS_TYPE_NAMED_PROXY`を設定し、`NULL`であれば`WINHTTP_ACCESS_TYPE_DEFAULT_PROXY`を設定します。
> `proxyConfig.lpszProxy ? WINHTTP_ACCESS_TYPE_NAMED_PROXY : WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,`

```cpp
// 現在ユーザのプロキシ情報を取得
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG proxyConfig;
ZeroMemory(&proxyConfig, sizeof(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG));
WinHttpGetIEProxyConfigForCurrentUser(&proxyConfig);

// WinHTTPオブジェクトの作成
HINTERNET hSession = WinHttpOpen(L"UserAgent/WinHTTP 1.0",
    proxyConfig.lpszProxy ? WINHTTP_ACCESS_TYPE_NAMED_PROXY : WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
    proxyConfig.lpszProxy, proxyConfig.lpszProxyBypass,
    0);
```

> [WinHttpGetIEProxyConfigForCurrentUser 関数 (winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/api/winhttp/nf-winhttp-winhttpgetieproxyconfigforcurrentuser)

## HTTPサーバーの証明書エラーを無視する
オプションフラグを設定し`WinHttpSetOption`関数を用いてリクエスト送信時の挙動を設定します。

```cpp
// HTTPリクエストの作成
HINTERNET hRequest = WinHttpOpenRequest(
    hConnect, L"GET", L"/index.html", NULL,WINHTTP_NO_REFERER,
    WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);

// 証明書エラーを無視するオプションを作成する
// SECURITY_FLAG_IGNORE_CERT_CN_INVALID: 証明書で無効なCommon Nameを許可
// SECURITY_FLAG_IGNORE_CERT_DATE_INVALID: 無効な証明書の日付 (有効期限が切れているか、まだ有効でない証明書) を許可
// SECURITY_FLAG_IGNORE_UNKNOWN_CA: 無効な証明機関を許可
DWORD dwFlags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA;

// 接続のオプションを設定する
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));

// HTTPリクエストを送信
BOOL bSent = WinHttpSendRequest(
    hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
```

> [オプション フラグ (Winhttp.h) - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/winhttp/option-flags)

## ファイルをダウンロードする
C++のメリットである`vector`クラスを利用して動的にバッファを確保、操作する方式を採用したものが以下となります。  
なお、`vector`クラスを利用しない場合は一定サイズのchar型配列のバッファを用意し、初期化、格納、ファイルへ追記を繰り返すことにより実現可能です。

```cpp
// データ受信
// 最終的なデータ用バッファ
vector<BYTE> dataBuffer;
DWORD dwSize = 0;
DWORD byteRead = 0;

// データの受信はチャンク単位での読み込みを行うためループする必要がある
do {

    // 受信
    if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
        wcout << L"Failed to WinHttpQueryDataAvailable: " << to_wstring(GetLastError()) << endl;
        break;
    }
    if (dwSize == 0) {
        break;
    }

    // 一時的なバッファ
    vector<BYTE> buffer(dwSize);
    if (!WinHttpReadData(hRequest, buffer.data(), dwSize, &byteRead)) {
        wcout << "Failed to WinHttpReadData: " << to_wstring(GetLastError()) << endl;
        break;
    }

    // 最終的なデータ用バッファに一時的なバッファの中身を追加する
    dataBuffer.insert(dataBuffer.end(), buffer.begin(), buffer.begin()+byteRead);
} while (dwSize > 0);

wcout << L"Total received data size: " << dataBuffer.size() << L" bytes" << endl;

// バッファからファイルに保存する
HANDLE hFile = CreateFile(filepath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
    wcerr << L"Failed to create file: " << filepath << endl;

    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);

    return 1;
}


// ファイルの書き込み
if (!WriteFile(hFile, dataBuffer.data(), (DWORD)dataBuffer.size(), NULL, NULL)) {
    wcerr << L"Failed to write to file." << endl;
}

// 後処理
CloseHandle(hFile);
```

# サンプルコード
## Webサイト閲覧
下記コードはHTTPS接続で`www.microsoft.com`にGETリクエストを送信し、受信したデータを読み取るサンプルプログラムです。  
小さいサイズの処理であれば下記サンプルコードを活用するのがよいでしょう。

```cpp
DWORD dwSize = 0;
  DWORD dwDownloaded = 0;
  LPSTR pszOutBuffer;
  BOOL  bResults = FALSE;
  HINTERNET  hSession = NULL, 
             hConnect = NULL,
             hRequest = NULL;

  // Use WinHttpOpen to obtain a session handle.
  hSession = WinHttpOpen( L"WinHTTP Example/1.0",  
                          WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                          WINHTTP_NO_PROXY_NAME, 
                          WINHTTP_NO_PROXY_BYPASS, 0 );

  // Specify an HTTP server.
  if( hSession )
    hConnect = WinHttpConnect( hSession, L"www.microsoft.com",
                               INTERNET_DEFAULT_HTTPS_PORT, 0 );

  // Create an HTTP request handle.
  if( hConnect )
    hRequest = WinHttpOpenRequest( hConnect, L"GET", NULL,
                                   NULL, WINHTTP_NO_REFERER, 
                                   WINHTTP_DEFAULT_ACCEPT_TYPES, 
                                   WINHTTP_FLAG_SECURE );

  // Send a request.
  if( hRequest )
    bResults = WinHttpSendRequest( hRequest,
                                   WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                                   WINHTTP_NO_REQUEST_DATA, 0, 
                                   0, 0 );


  // End the request.
  if( bResults )
    bResults = WinHttpReceiveResponse( hRequest, NULL );

  // Keep checking for data until there is nothing left.
  if( bResults )
  {
    do 
    {
      // Check for available data.
      dwSize = 0;
      if( !WinHttpQueryDataAvailable( hRequest, &dwSize ) )
        printf( "Error %u in WinHttpQueryDataAvailable.\n",
                GetLastError( ) );

      // Allocate space for the buffer.
      pszOutBuffer = new char[dwSize+1];
      if( !pszOutBuffer )
      {
        printf( "Out of memory\n" );
        dwSize=0;
      }
      else
      {
        // Read the data.
        ZeroMemory( pszOutBuffer, dwSize+1 );

        if( !WinHttpReadData( hRequest, (LPVOID)pszOutBuffer, 
                              dwSize, &dwDownloaded ) )
          printf( "Error %u in WinHttpReadData.\n", GetLastError( ) );
        else
          printf( "%s", pszOutBuffer );

        // Free the memory allocated to the buffer.
        delete [] pszOutBuffer;
      }
    } while( dwSize > 0 );
  }


  // Report any errors.
  if( !bResults )
    printf( "Error %d has occurred.\n", GetLastError( ) );

  // Close any open handles.
  if( hRequest ) WinHttpCloseHandle( hRequest );
  if( hConnect ) WinHttpCloseHandle( hConnect );
  if( hSession ) WinHttpCloseHandle( hSession );
```
> [WinHTTP セッションの概要 | Web からリソースをダウンロードする - Win32 apps](https://learn.microsoft.com/ja-jp/windows/win32/winhttp/winhttp-sessions-overview#downloading-resources-from-the-web)

## ファイルのダウンロード
データサイズが4Kバイト以上の場合は下記サンプルコードのようにループ処理を実装する必要があります。

```cpp
#include <windows.h>
#include <winhttp.h>
#include <iostream>
#include <string>
#include <vector>
#include <shlwapi.h>

#pragma comment (lib, "winhttp.lib")
#pragma comment(lib, "Shlwapi.lib")
using namespace std;

int main() {
	const wchar_t* url = L"download.sysinternals.com";
	const wchar_t* path = L"files/ProcessMonitor.zip";
	const wchar_t* user_agent = L"UserAgent/WinHTTP 1.0";
	const wchar_t* filepath = L"C:\\Users\\user\\";
	

	// WinHTTPオブジェクトの作成
	HINTERNET hSession = WinHttpOpen(user_agent, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
	if (hSession == NULL) {
		wcout << L"Failed to WinHttpOpen: " << to_wstring(GetLastError()) << endl;
		return 1;
	}

	// サーバーに接続するための準備
	HINTERNET hConnect = WinHttpConnect(hSession, url, INTERNET_DEFAULT_HTTPS_PORT, 0);
	if (hConnect == NULL) {
		wcout << L"Failed to WinHttpConnect: " << to_wstring(GetLastError()) << endl;
		WinHttpCloseHandle(hSession);

		return 1;
	}

	// HTTPリクエストの作成
	HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
	if (hRequest == NULL) {
		wcout << L"Failed to WinHttpOpenRequest: " << to_wstring(GetLastError()) << endl;

		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);

		return 1;
	}

	// HTTPリクエストの送信
	BOOL bSent = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
	if (!bSent) {
		wcout << L"Failed to WinHttpSendRequest: " << to_wstring(GetLastError()) << endl;

		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);

		return 1;
	}

	// レスポンスの受信
	BOOL bReceived = WinHttpReceiveResponse(hRequest, NULL);
	if (!bReceived) {
		wcout << L"Failed to WinHttpReceiveResponse: " << to_wstring(GetLastError()) << endl;

		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);

		return 1;
	}

	// データ受信
	// 最終的なデータ用バッファ
	vector<BYTE> dataBuffer;
	DWORD dwSize = 0;
	DWORD byteRead = 0;

	// データの受信はチャンク単位での読み込みを行うためループする必要がある
	do {

		// 受信
		if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
			wcout << L"Failed to WinHttpQueryDataAvailable: " << to_wstring(GetLastError()) << endl;
			break;
		}
		if (dwSize == 0) {
			break;
		}

		// 一時的なバッファ
		vector<BYTE> buffer(dwSize);
		if (!WinHttpReadData(hRequest, buffer.data(), dwSize, &byteRead)) {
			wcout << "Failed to WinHttpReadData: " << to_wstring(GetLastError()) << endl;
			break;
		}

		// 最終的なデータ用バッファに一時的なバッファの中身を追加する
		dataBuffer.insert(dataBuffer.end(), buffer.begin(), buffer.begin()+byteRead);
	} while (dwSize > 0);

	// 受信した合計データサイズを出力
	wcout << L"Total received data size: " << dataBuffer.size() << L" bytes" << endl;

	// URLからファイル名を取得
	wstring fileName = PathFindFileNameW(path);
	wstring fullPath = filepath + fileName;

	// バッファからファイルに保存する
	HANDLE hFile = CreateFile(fullPath.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		wcerr << L"Failed to create file: " << filepath << endl;

		WinHttpCloseHandle(hRequest);
		WinHttpCloseHandle(hConnect);
		WinHttpCloseHandle(hSession);

		return 1;
	}


	// ファイルの書き込み
	if (!WriteFile(hFile, dataBuffer.data(), (DWORD)dataBuffer.size(), NULL, NULL)) {
		wcerr << L"Failed to write to file." << endl;
	}


	// 後処理
	CloseHandle(hFile);
	WinHttpCloseHandle(hRequest);
	WinHttpCloseHandle(hConnect);
	WinHttpCloseHandle(hSession);

	return 0;
}
```