C++(Win32)でPOSTしたいときの方法を紹介します。Win32ではWinINetを使用すると、比較的かんたんにPOSTすることができます。当記事では、WinINetを使用したPOSTの方法をサンプルコードとあわせて紹介していきます。
サンプルコード
さっそくサンプルコードを公開します。
URL「http://192.168.1.4/test/test.php」に、次のデータをPOSTする例です。
- キー「a」値「1234」
- キー「b」値「5678」
// ConsoleApplication1.cpp : このファイルには 'main' 関数が含まれています。プログラム実行の開始と終了がそこで行われます。
//
#include <iostream>//std::coutなど使うのに必要
#include <tchar.h>//_Tを使うのに必要
#include <windows.h>//wininet.hを使うのに必要
#include <wininet.h>//HTTP通信に必要
//追加の依存ファイルにwininet.libを設定
#pragma comment (lib, "wininet.lib")
int main()
{
// インターネット接続のハンドルを作成
HINTERNET hInternet = InternetOpen(
NULL, //User-Agentに設定したい文字列を指定する
INTERNET_OPEN_TYPE_PRECONFIG,//接続方法(直接接続・プロキシ使用)を指定する
NULL,//プロキシサーバーのアドレスを指定する
NULL,//プロキシサーバー使用時、プロキシを経由させないで直接接続するアドレスを指定する
0//オプション
);
// インターネット接続を確立して、コネクトハンドルを得る
HINTERNET hConnect = InternetConnect(
hInternet,
_T("192.168.1.4"),//サーバーアドレス
INTERNET_DEFAULT_HTTP_PORT,//ポート番号を指定する
NULL,//認証ユーザー名を指定する
NULL,//認証パスワードを指定する
INTERNET_SERVICE_HTTP,//サービスタイプ(HTTP・FTP)を指定する
0,//FTP接続時、パッシブモードにするかどうか指定する
0
);
// リクエストを開いて、リクエストハンドルを得る
HINTERNET hRequest = HttpOpenRequest(
hConnect,
_T("POST"),//POSTにする
_T("/test/test.php"), //ホスト名以下
_T("HTTP/1.1"),//HTTPプロトコルバージョン
NULL, NULL, INTERNET_FLAG_RELOAD, 0);
// ヘッダーを設定する
wchar_t header[] = L"Content-Type: application/x-www-form-urlencoded; charset=utf-8";//コンテンツタイプをFORMに指定
//送信データを指定する
char data[] = "a=1234&b=5678";
// リクエストを送信する
BOOL r = HttpSendRequest(
hRequest,//リクエストハンドルを指定する
header, //ヘッダーを指定する
wcslen(header), //ヘッダーのサイズを求めて指定する
(LPVOID)(data),//送信コンテンツを指定する
strlen(data) //送信コンテンツのサイズを求めて指定する
);
// エラーがあれば表示する
std::cout << GetLastError() << std::endl;
// ハンドルを閉じる
InternetCloseHandle(hInternet);
}
実行すると、サーバーに次のようなデータが届きます。
2020-12-10 12:15:08 Array //リクエストヘッダー
(
[Content-Type] => application/x-www-form-urlencoded
[Host] => 192.168.1.4
[Content-Length] => 13
[Cache-Control] => no-cache
)
2020-12-10 12:15:08 Array //POSTデータ
(
[a] => 1234
[b] => 5678
)
きちんとPOSTできていますね。
▼PHPでリクエストヘッダとPOSTされたデータを書き出す方法は、次のページで紹介しています。
解説
C#(.NET)なら、WebRequestを使ってかんたんにPOSTできますが、C++ではWinINetを使用します。
WinINetのほかにWinHTTPというAPIもありますが、通常は使用する必要はありません。WinHTTPのWebクライアントとしての機能は、WinINetに内包されています。よって、クライアントとしてPOSTするだけなら、基本的にWinINetを使用すればOKです。逆に、HTTPサーバーを構築するときは、WinHTTPを使用する必要があります。
次のページにWinINetとWinHTTPの機能比較表が掲載されていますが、利用可能な機能もWinINetのほうが多いです。WinINetなら、HTTPのみならず、FTPも使用することができます。
処理の流れ
まず、全体の流れを整理します。
- InternetOpen・・・初期化
- InternetConnect・・・セッション開始
- HttpOpenRequest・・・リクエスト作成
- HttpSendRequest・・・データの送信
最初にInternetOpen関数を実行します。
void InternetOpenW(
LPCWSTR lpszAgent,
DWORD dwAccessType,
LPCWSTR lpszProxy,
LPCWSTR lpszProxyBypass,
DWORD dwFlags
);
InternetOpen関数を実行することで、WinINetが初期化されます。初期化が完了すると、ハンドルが返されます。以降、ここで得られたハンドルを使用して、処理を行っていきます。
ギモン void型なのにHANDLEが返るってどういうこと?
C++のvoid型というのは、「型指定なしのポインタ(void*型)」のことを指しています。
HANDLE型も、実体は型指定なしのポインタ(すなわちvoid*型)です。よって、HANDLEを返す関数の返り値はvoid型であるといえます。
C#に慣れていると、「void型=返り値なし」という認識になりがちですが、C++では何も返さないとは限らないので、注意が必要です。
Unicode対応版としてビルドした場合、InternetOpen関数をはじめ、ほとんどの関数はワイド文字対応版の関数(末尾に「W」がついている関数)に置き換えられます。(既定ではUnicode対応版としてビルドされます。)よって、InternetOpen関数とInternetOpenW関数は同じものとして考えてもらってかまいません。
次にInternetConnect関数を実行します。
void InternetConnectW(
HINTERNET hInternet,
LPCWSTR lpszServerName,
INTERNET_PORT nServerPort,
LPCWSTR lpszUserName,
LPCWSTR lpszPassword,
DWORD dwService,
DWORD dwFlags,
DWORD_PTR dwContext
);
これにより、サーバーに接続が行われ、セッションが開始されます。
あとは、HTTPOpenRequest関数とHTTPSendRequest関数により、データをPOSTします。
// リクエストを開いて、リクエストハンドルを得る
HINTERNET hRequest = HttpOpenRequest(
hConnect,
_T("POST"),//POSTにする
_T("/test/test.php"), //ホスト名以下
_T("HTTP/1.1"),//HTTPプロトコルバージョン
NULL, NULL, INTERNET_FLAG_RELOAD, 0);
// ヘッダーを設定する
wchar_t header[] = L"Content-Type: application/x-www-form-urlencoded";//コンテンツタイプをFORMに指定
//送信データを指定する
char data[] = "a=1234&b=5678";
// リクエストを送信する
BOOL r = HttpSendRequest(
hRequest,//リクエストハンドルを指定する
header, //ヘッダーを指定する
wcslen(header), //ヘッダーのサイズを求めて指定する
(LPVOID)(data),//送信コンテンツを指定する
strlen(data) //送信コンテンツのサイズを求めて指定する
);
POSTでデータを送信する際には、何らかのMIMEタイプにデータをエンコードする必要があります。jsonなどをはじめ、使用できるMIMEタイプは多岐にわたりますが、今回はformタグを送信する際に使用される「application/x-www-form-urlencoded」を採用します。この場合、送信データは「キー=値」のペアを&で区切る形でエンコードされますので、送信データもそれに従って定義しておきます。
//送信データを指定する
char data[] = "a=1234&b=5678";
リクエストヘッダーのContent-typeにも、「application/x-www-form-urlencoded」を指定しておきます。これでサーバーは受け取ったデータを適切にパースしてくれるようになります。
// ヘッダーを設定する
wchar_t header[] = L"Content-Type: application/x-www-form-urlencoded";//コンテンツタイプをFORMに指定
リクエストの送信が完了したら、ハンドルを破棄しましょう。
//ハンドルを閉じる(hRequestなどは同時に閉じられるのでClose不要)
InternetCloseHandle(hInternet);
当プログラムでは、複数のハンドルが使用されていますが、hInternetハンドルだけ閉じれば十分です。二次的に生成されているhRequestなどのハンドルは、自動的に同時に閉じられます。
POSTによる送信については以上です。エラーはGetLastError関数で取得できますので、うまくいかないときは追加してみてください。
レスポンスを取得するには?
POSTした後、サーバーからのレスポンスを取得したいケースもあるでしょう。サーバーからのレスポンスは、hRequestハンドルにFileとして格納されています。
InternetReadFile関数を使用すると、格納されているレスポンスデータを読み出すことができます。
サンプルコード
InternetCloseHandle(hInternet);
の前に、次のコードを追加してください。
//レスポンス取得・表示
char buf[1024] = { 0 };//バッファ
DWORD size = 1024;//1回あたり読み出すサイズ
BOOL bResult;
DWORD lpdwReadLength;
for (;;) {
//レスポンスを取得する
bResult = InternetReadFile(hRequest, buf, sizeof(buf), &lpdwReadLength);
//読み出しサイズ0以下のとき、ループを抜ける
if (lpdwReadLength <= 0) {
break;
}
//バッファを出力する
printf("%s", buf);
//std::cout << buf;
}
// エラーがあれば表示する
std::cout << GetLastError() << std::endl;
//ハンドルを閉じる(hRequestなどは同時に閉じられるのでClose不要)
InternetCloseHandle(hInternet);
これを追加して実行すると、コンソールにレスポンスが出力されます。
解説
InternetReadFile関数は、hRequestハンドルの中に格納されているレスポンスボディを読み出します。
BOOLAPI InternetReadFile(
HINTERNET hFile,
LPVOID lpBuffer,
DWORD dwNumberOfBytesToRead,
LPDWORD lpdwNumberOfBytesRead
);
第3引数に、1回あたり読み出すバイト数を指定します。読み出されたデータ(文字配列)は、第2引数に出力されます。第2引数に文字型配列(char[])を指定することで、指定したバイト数だけ、サーバーからのレスポンスを文字列として取り出せるという仕組みです。これをforでループさせて、レスポンス全体を取得します。
使用する変数は、最初に定義してあります。
char buf[1024] = { 0 };//バッファ
DWORD size = 1024;//1回あたり読み出すサイズ
BOOL bResult;
DWORD lpdwReadLength;
NULL終端
Cでの文字列は、必ずNULLで終わっていなければならないというルールがあります。これをNULL終端といいます。
今回、バッファとして使用する配列(buf)は、変数定義の時点でゼロクリアしておきました。buf[1024]={0}
とは、次のコードを実行しているのとほぼ同義です。
for (int i = 0; i < 1024; i++) {
buf[i] = 'for (int i = 0; i < 1024; i++) {
buf[i] = '\0';
}
';
}
これをせずに、ただの空配列としてbufを定義してしまうと、bufがレスポンスデータで埋めつくされない場合に問題が出ます。末尾の要素が不定となってしまい、以下のような実行結果になってしまいます。
NULL終端に関わる部分は、脆弱性の元となることもあります。次のページに詳しい解説がなされていますので、興味があれば参照してみてください。
ステータスコードを取得するには?
ステータスコードなど、各種情報を取得したいときにはHttpQueryInfo関数を使用します。
例)ステータスコードを取得する
//ステータスコード取得
DWORD buf2 = { 0 };
DWORD buf2len = sizeof(buf2);
HttpQueryInfoW(hRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &buf2, &buf2len, 0);
printf("%d", buf2);
実行すると、ステータスコードが表示されます。
404
ステータスコード以外にも、HttpQueryInfo関数を使用すると、ヘッダーに含まれる様々な情報を取得することができます。第2引数にパラメーターを指定することで、取得するデータを切り替えることができます。指定できるパラメータは、次のページにまとめられています。
日本語のデータを送るには?
基本的にこのままでOKです。data変数に日本語を追加してもらえれば、日本語も送れるはずです。
char data[] = "a=1234&b=5678&あ=あいうえお";
実際にやってみたところ、日本語も送信することができました。
WinINetのオプションを活用する
WinINetには、さまざまなオプションが用意されています。オプションを設定するにはInernetSetOption関数を使用します。タイムアウト時間を変更したり、接続失敗時に自動的にリトライするようにしたりすることができるようになります。
BOOL InernetSetOption( IN HINTERNET hInternet OPTIONAL, IN DWORD Option, IN LPVOID Buf, IN DWORD BufSize);
第1引数には、hInternetハンドルを指定します。InternetOpen関数で返されるハンドルです。
タイムアウト時間を変更する
Optionに”INTERNET_OPTION_CONNECT_TIMEOUT”を指定すると、タイムアウト時間を任意に設定することができます。
InternetSetOption(hInternet, INTERNET_OPTION_CONNECT_TIMEOUT, &dwtimeoutMs, sizeof(timeoutMs));
第3引数にタイムアウトまでの時間をミリ秒単位で指定します。指定時間内にサーバーとの接続が確立できないと、エラー12002(ERROR_INTERNET_TIMEOUT)が発生します。
接続できないときに自動で再試行するようにする
INTERNET_OPTION_CONNECT_RETRIESオプションを設定すると、接続失敗時に自動再試行させることもできます。
InternetSetOption(hInternet, INTERNET_OPTION_CONNECT_RETRIES , &dwRetryTimes, sizeof(dwRetryTimes));
自動再試行する回数や間隔を任意の値に設定することができます。
ほかにも様々なオプションが用意されています。MSDNに指定できるパラメーターが掲載されていますので、ご参照ください。
まとめ
.NET Frameworkの活用が当たり前になっている世の中で、「今頃C++??」という印象を受ける方もいるかもしれません。
しかし、実際動かしてみると、C#とは比較にならないくらい、爆速で動作します。一瞬で処理が完了するので、とにかく速く動作させたいというニーズには、今でもぴったりな選択肢といえるでしょう。
それに、メモリを初期化したりなど、C#では味わえない”コンピューターをコントロールしている感覚”がまた新鮮です。きっちり理解していないと動作させられないので、勉強になります。ふだんHTTPClientを使用されている方も、たまにはC++で遊んでみてはいかがでしょうか。
WinINetに関しては、Microsoft Docsに詳細が掲載されています。あわせて参照することをおすすめします。
コメント