Win32/64ネットワーク開発 002 wininetを使ったhttp通信
概要
またまた、あまり使い道が思いつかないライブラリを使ってみます。wininetライブラリというものです。通信してるなぁって感じはします。
VisualStudioでソリューションエクスプローラからプロジェクト名の項目を選択した状態で、メニューの[プロジェクト(P)]-[プロパティ(P)]を選択して表示されるダイアログボックスの[リンカー]-[入力]から[追加の依存ファイル]の項目に「wininet.Lib」を追記します。追記するときは「;」がデリミタ(区分け記号)として必要ですので、規定値を消さずに、追記するには、
$(CoreLibraryDependencies);%(AdditionalDependencies);wininet.Lib
のような形式になるでしょう。またプログラムを書くときには#include <wininet.h>のようにしてwininet.hファイルをインクルードする必要があります。ここでは、基本のアプリ(Win32/64アプリ開発 001 基礎、Win32/64アプリ開発 002 基礎の理解、Win32/64アプリ開発 002 基礎の理解)にメニューを追加して、選択した項目からダイアログを表示して、URLの入力を受け取りwininetライブラリの関数を使ってみたいと思います。
メニュー追加から説明ですね。イチカラじゃないのがやさしくないよね。わかってる。でも基本のアプリは紹介したから、そこを見てほしいんですよね。
メニューを追加するには、リソースエディタでメニューを作成します。ソリューションエクスプローラーに表示されている「****.rc」という項目をダブルクリック選択して、リソースビュータブに切り替えます。リソースの項目が一つでもコード表示されているとリソースビューのUI表示可能状態にはならないので、そこには注意が必要です。言葉で説明すると難しい複雑さがありますね。リソースビューのトップフォルダを右クリックして表示されるメニューから「リソースの追加」を選択します。表示されたダイアログで「MENU」を選択します。リソースビューでこれから作成するメニューを青色選択状態にすると、プロパティビューに表示された項目IDの欄の値が編集できます。ID_MENUという名前にしましょうか?MENUでもいいですけど今後の説明でMENUと記述したときにIDのことをいっているのかMENUという機能の話をしているかわからなくなるので、ID_MENUとさせていただきました。
編集ビューでUIを伴って、メニューが作れます。直感的に操作できます。ここに入力とあるところに文字列を入力すると、メニューの外観が入力した通りになります。メニューを選択したときに選択した項目が何であるかを意味するIDというものを設定する必要があります。ここに入力とあるところに入力した文字はメニュー項目のキャプションと呼びます。それは、プロパティビューでも確認できるようになっています。項目を青色選択している状態では、プロパティビューのキャプションという項目があって、今まさに入力した内容が反映されるようになっています。キャプションの欄に次のように入力してみましょう。「URL(&U)...」このように(&U)のような指定をするとメニュー選択ショートカットとして振る舞います。メニューはキーボードで操作しやすくなり、[Alt]→[U]のように操作するとメニューが選択できるようになります。...は選択するとまだ処理が即実行されず、さらに何か聞いてくるダイアログが開くなりするだけの処理だよという動作を意味しています。...もつけておくと親切でしょう。キャプションを「URL(&U)...」としたところと同じ部分にIDという項目もあります。これがメニュー項目を選択したときのIDになります。ここでは「IDM_URL」のように名前をつけておきましょう。これでリソースの保存をするために保存ボタンを押すか、[Ctrl]+[s]と操作をしましょう。
そして、WNDCLASS型の変数に各情報を設定してRegisterClassEx関数で登録する部分で
WNDCLASS WNDCLASSwc = {0};
…
WNDCLASSwc.lpszMenuName = L"ID_MENU";
…
のようにメニューのIDを指定します。
次にDialog、選択した後に開くダイアログを作りましょう。リソースの作成から先ほどは[MENU]と選択したところを、[Dialog]-[IDD_PROPPAGE_SMALL]と選択します。Dialogが作成されます。
いろいろ操作してコントロールを配置したいところですが、説明するのも、作る方も大変なので、コードを打ち込んだ方が楽なのでそのようにしてみましょう。リソース系の編集ウィンドウをすべて閉じて、ソリューションエクスプローラから「****.rc」を右クリックして表示されるメニューから[コードの表示]を選択します。リソースビューを閉じるかを聞かれたら閉じるを選択します。そしたら、コードが表示されるはずです。これが、UIを使って操作してできたリソースコードの本体です。所詮はただの文字列です。このなかから、
IDD_PROPPAGE_SMALL DIALOGEX …
…
…
END
というカタマリを見つけて、以下のように打ち変えます。
ID_DIALOG_URL DIALOGEX 0, 0, 256, 47
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "URL"
FONT 9, "MS ゴシック", 400, 0, 0x80
BEGIN
EDITTEXT IDC_URL,39,7,210,13,ES_AUTOHSCROLL
PUSHBUTTON "閉じる",IDCANCEL,149,27,72,13
LTEXT "URL",IDC_STATIC,7,7,23,10
DEFPUSHBUTTON "接続",IDC_CONNECT,34,27,72,13
END
のようにします。これで保存して、エディタを閉じて再度リソースエディタで開いてみるとダイアログが完成しています。URLを入力する欄と、ボタンが2つと、入力欄の横のテキスト(スタティックテキストと呼びます)があることが確認できます。UIでやると手数が増えることもあるということですね。このようにUIを使いこなす手順と、リソースをコードで直接編集する手順を知っていると楽ができます。ただし、リソースのコードがVisual Studioが読み取れないほどにルールに反していると二度とリソースをコンパイルすることも、エディタでみることもできなくなりますので、深い理解も必要です。
更に、コードを変更していきます。メニューから飛んできたIDM_URLを受け取ったら、ダイアログを表示するようにコードを追加します。
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
switch(msg){
…
case WM_COMMAND:
switch (LOWORD(wp)) {//wpはMainウィンドウプロシージャーが受け取る第3引数のWPARAM型
case IDM_URL:
DialogBox(HINSTANCEinst,//HINSTANCEinstはWinMain関数の第1引数の値。
L"ID_DIALOG_URL",
hWnd,//hWndはMainウィンドウプロシージャーが受け取る第1引数のHWND型
(DLGPROC)UrlDialogProc);
break;
}
break;
…
}
}
このようにDialogBox関数でリソースで定義したID_DIALOG_URLというダイアログが呼び出せます。ダイアログ用のプロシージャ関数を定義しましたので、
BOOL CALLBACK UrlDialogProc(HWND, UINT, WPARAM, LPARAM);
というプロトタイプ宣言や
BOOL CALLBACK UrlDialogProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp){
switch (msg) {
…
}
}
のようなダイアログメッセージプロシージャ関数本体が必要です。そうして、インターネット接続をして、urlが示すアドレスにhttp応答を呼び掛けて、受け取るという処理を行います。
wchar_t pwchStrUrl[2083] = "";//IEのURL最大文字数 グローバル変数
BOOL CALLBACK UrlDialogProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp){
switch (msg) {
case WM_INITDIALOG:
SetDlgItemText(hDlg, IDC_URL, pwchStrUrl);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wp)) {
case IDC_CONNECT:
GetDlgItemText(hDlg, IDC_URL, pwchStrUrl, (int)sizeof(pwchStrUrl));
EndDialog(hDlg, IDOK);
HttpConnect();//いよいよのインタネットのhttp通信を行うこれから記述予定のユーザ指定関数の呼び出しです。
return TRUE;
case IDCANCEL:
EndDialog(hDlg, IDCANCEL);
return TRUE;
}
return FALSE;
}
return FALSE;
}
と上記のように、pchStrUrlという文字列グローバル変数(これは暫定的な簡単なやり方です。文字列グローバル変数をWin32/64でうまく管理する記事はこちらに記述します。)にURLの文字列をダイアログの接続ボタンを押した後に受け取るようにします。
ダイアログにあるテキストエディットボックスと文字列をやり取りするのは以下のような関数を使います。
■SetDlgItemText(HWND hDlg, int IDC_URL, LPWSTR pchStrUrl)
第1引数: HWND hDlg
ダイアログボックスのウィンドウハンドルです。通常はダイアログメッセージプロシージャ関数の第1引数で受け取ったものです。
第2引数: int IDC_URL
ダイアログボックスにあるコントロールのidに割り当てられている数値です。
第3引数: LPWSTR pwchStrUrl or LPSTR pchStrUrl
ダイアログボックスの指定のテキストエディットボックスに設定するワイド文字列かマルチバイト文字列変数です。
■GetDlgItemText(hDlg, IDC_URL, pchStrUrl, (int)sizeof(pchStrUrl));
第1引数: HWND hDlg
ダイアログボックスのウィンドウハンドルです。通常はダイアログメッセージプロシージャ関数の第1引数で受け取ったものです。
第2引数: int IDC_URL
ダイアログボックスにあるコントロールのidに割り当てられている数値です。
第3引数: LPWSTR pwchStrUrl or LPSTR pchStrUrl
ダイアログボックスの指定のテキストエディットボックスから受け取るワイド文字列かマルチバイト文字列変数です。
第4引数: int cchMax
第3引数の変数のバイトサイズを指定します。
本題
ここからがネットワーク開発の本題になります。ここまではネットワーク開発の動作を確かめるための基本的なWin32/64アプリ開発の操作に過ぎませんでした。
URL接続ボタンを押された後、HttpConnect();という関数を呼び出していますので、この関数の中に処理を記述していきます。
プロトタイプ宣言は
int HttpConnect();
のように定義すればいいです。
pwchStrUrlに取得したURLのページの内容がUTF-8で書かれているなら、以下の内容でHGLOBAL型のHGLOBALwpageに文字化けすることなく受け取ることができます。グローバル変数のHGLOBALは文字列のようなものを格納するのに向いているvoid型の変数です。
…
HGLOBAL HGLOBALwpage;
wchar_t* pwchpage;
int HttpConnect(){
HINTERNET HINTERNETinet, HINTERNETurl;
char pchStrUrl[2048];
DWORD DWORDread;
char pchStrBuf[1025];
char* pchStrSrc;
int iTotal = 0;
HGLOBAL HGLOBALmem;
HGLOBALmem = GlobalAlloc(GHND, 1);
pchStrUrlLen = WideCharToMultiByte(932, 0, pwchStrUrl, -1, NULL, 0, NULL, NULL)
WideCharToMultiByte(932, 0, pwchStrUrl, -1, pchStrUrl, pchStrUrlLen, NULL, NULL);
HINTERNETinet = InternetOpen(L"yo-net.jp", INTERNET_OPEN_TYPE_PRECONFIG,
NULL, NULL, 0);
HINTERNETurl = InternetOpenUrl(HINTERNETinet, pchStrUrl, NULL, 0, 0, 0);
while (1) {
InternetReadFile(HINTERNETurl, pchStrBuf, (DWORD)sizeof(pchStrBuf) - 1, &DWORDread);
pchStrUrl[DWORDread] = '\0';
if (DWORDread == 0)
break;
iTotal += DWORDread;
HGLOBALmem = GlobalReAlloc(HGLOBALmem, (SIZE_T)iTotal + 1, GMEM_MOVEABLE);
pchStrSrc = (char*)GlobalLock(HGLOBALmem);
strcat_s(pchStrSrc, (SIZE_T)nTotal + 1, pchStrBuf);
}
HGLOBALwpage = GlobalAlloc(GHND, ((SIZE_T)iTotal * sizeof(wchar_t)) + 1);
pwchpage = (wchar_t*)GlobalLock(HGLOBALwpage);
int iLen = MultiByteToWideChar(CP-UTF8, 0, pchStrSrc, -1, NULL , 0);
MultiByteToWideChar(CP-UTF8, 0, pchStrSrc, -1, pwchpage , iLen);
(wchar_t*)GlobalUnlock(HGLOBALwpage);
(char*)GlobalUnlock(HGLOBALmem);
GlobalFree(HGLOBALmem)
InternetCloseHandle(HINTERNETurl);
InternetCloseHandle(HINTERNETinet);
}
…
15行目までは、必要な変数の定義や、URLがワイド文字列系の変数pwchStrUrlに格納されているので、SJISに変換してpchStrUrlに移し替えています。文字列の変換に関してはWin32/64技術 001 マルチバイト文字列→ワイド文字列変換の記事やWin32/64技術 003 ワイド文字列→マルチバイト文字列変換の記事で理解を深めて下さい。HGLOBAL型変数の使い方はコチラの記事を参照して下さい。
16行目からがHTTP通信を始める処理になります。
1.InternetOpenでHTTP通信をする宣言をしたHINTERNETinet変数を準備します。
2.InternetOpenUrlに準備した変数を使いながら、指定したURLに対してHTTP応答を求めます
3.InternetReadFileでBufferサイズ分だけ応答を読み込むという処理をして、繰り返しInternetReadFileを呼び出すと、読み取った分の途中からまた応答を読み込めるようになっているので、この仕組みを使いながら全ての応答によって得た文字列を読み取ります。
wininetライブラリを使ってHTTP通信をするときはこんな感じでやりとりするだけです。HTMLを読み取って整形処理をして、表示する処理はプログラマが組み込まなければなりません。世に出回っているブラウザは標準化されているHTML、CSS、Javascriptに対応しています。ものすごく大変なことをやってのけていますね。
■InternetOpen(LPCWSTR, DWORD, LPCWSTR, LPCWSTR, DWORD)
第1引数:LPCWSTR lpszAgent
通信をしているプログラムに名前を付けることになっています。どういう手段で、通信をしているのかを記録として残す仕組みがあります。
第2引数:DWORD dwAccessType
必要なアクセスの種類のフラグを設定します。
- INTERNET_OPEN_TYPE_DIRECT:すべてのホスト名をローカルで解決します。
- INTERNET_OPEN_TYPE_PRECONFIG:レジストリからプロキシまたは直接構成を取得します。オススメ
- INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY:レジストリからプロキシまたは直接構成を取得し、スタートアップ Microsoft JScript またはインターネット セットアップ (INS) ファイルを使用できないようにします。
- INTERNET_OPEN_TYPE_PROXY:プロキシ バイパス リストが指定され、解決される名前がプロキシをバイパスしない限り、プロキシに要求を渡します。 この場合、関数は INTERNET_OPEN_TYPE_DIRECT を使用します。
第3引数:LPCWSTR lpszProxy
INTERNET_OPEN_TYPE_PROXY に設定してプロキシアクセスを指定するときに使用するプロキシサーバーの名前を指定する null で終わる文字列へのポインター。
第4引数:LPCWSTR lpszProxyBypass: INTERNET_OPEN_TYPE_PROXY に設定されている場合にプロキシ経由でルーティングされない、ホスト名または IP アドレスの省略可能なリスト、またはその両方を指定する null で終わる文字列へのポインター。
第5引数:DWORD dwFlags
- INTERNET_FLAG_ASYNC:この関数から返されたハンドルから派生したハンドルに対して非同期要求のみを行います。
- INTERNET_FLAG_FROM_CACHE:ネットワーク要求を行いません。 すべてのエンティティがキャッシュから返されます。
- INTERNET_FLAG_OFFLINE:ネットワーク要求を行いません。 すべてのエンティティがキャッシュから返されます。INTERNET_FLAG_FROM_CACHEと同じことです。
InternetOpen関数は その他のWinINet関数に渡す有効なHINTENETハンドルを返します。
■InternetOpenUrl(HINTERNET, LPCWSTR, LPCWSTR, DWORD, DWORD, DWORD)
第1引数:HINTERNET hInternet
InternetOpen関数を実行して得られたインターネットセッションへのハンドル。
第2引数:LPCWSTR lpszUrl
取得するhttp応答を得るウェブサーバへのアドレスを指定する
第3引数:LPCWSTR lpszHeaders
HTTP サーバーに送信されるヘッダーを指定する null で終わる文字列へのポインター
第4引数:DWORD dwHeadersLength
追加ヘッダーのサイズ、lpszHeaders が NULLでないときに -1が指定された場合は \0文字でおわる文字列として計算されます。
第5引数:DWORD dwFlags
HTTP要求に関する詳細な設定をフラグ値にしたものです。ものすごい沢山の種類のフラグがあります。https://learn.microsoft.com/ja-jp/windows/win32/api/wininet/nf-wininet-internetopenurlaに詳細が記述されています。
第6引数:DWORD_PTR dwContext
返されるハンドルと共にコールバック関数に渡されるアプリケーション定義値を指定する変数へのポインター。一瞬なんのことをいっているかわからない説明だったと思いますが、DWORD dwContext=123(関数はDWORD_PTRの形式を必要としているので、&dwContextのように設定します)のように特定の識別値を与えることで、このあと使う関数で、この値によって複数の通信を切り分けて使うのに利用する目的で準備されている仕組みです。wininetを使うときにそこまで複雑な通信をするとは思えませんが、winsockなどで通信するときも、こういった複数のハンドル値を管理するうまい仕組みが必要だという予備知識にもなるかと思います。切り分けをするためにもこういった使い方をする手法があるということくらいは頭の隅においておいたほうがいいかもしれません。この仕組みを使わないで、適当に0と設定しておいてもいいわけです。