「Win32/64ネットワーク開発 002 wininetを使ったhttp通信」の版間の差分

提供:yonewiki
編集の要約なし
 
(同じ利用者による、間の24版が非表示)
1行目: 1行目:
[[Win32/64 ネットワーク処理開発]]へ戻る。
[[Win32/64 ネットワーク処理開発]]へ戻る。
前の記事:[[Win32/64ネットワーク開発 001 ATLを使ったブラウザ]]
次の記事:[[Win32/64ネットワーク開発 003 wininetを使ったftp通信]]


== '''概要''' ==
== '''概要''' ==
 またまた、あまり使い道が思いつかないライブラリを使ってみます。wininetライブラリというものです。通信してるなぁって感じはします。
 またまた、あまり使い道が思いつかないライブラリを使ってみます。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関数で登録する部分で
<syntaxhighlight lang="cpp">
WNDCLASS WNDCLASSwc = {0};
WNDCLASSwc.lpszMenuName = L"ID_MENU";
</syntaxhighlight>
のようにメニューのIDを指定します。
次にDialog、選択した後に開くダイアログを作りましょう。リソースの作成から先ほどは[MENU]と選択したところを、[Dialog]-[IDD_PROPPAGE_SMALL]と選択します。Dialogが作成されます。
 いろいろ操作してコントロールを配置したいところですが、説明するのも、作る方も大変なので、コードを打ち込んだ方が楽なのでそのようにしてみましょう。リソース系の編集ウィンドウをすべて閉じて、ソリューションエクスプローラから「****.rc」を右クリックして表示されるメニューから[コードの表示]を選択します。リソースビューを閉じるかを聞かれたら閉じるを選択します。そしたら、コードが表示されるはずです。これが、UIを使って操作してできたリソースコードの本体です。所詮はただの文字列です。このなかから、
IDD_PROPPAGE_SMALL DIALOGEX …
END
 というカタマリを見つけて、以下のように打ち変えます。
<syntaxhighlight lang="cpp">
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
</syntaxhighlight>
 のようにします。これで保存して、エディタを閉じて再度リソースエディタで開いてみるとダイアログが完成しています。URLを入力する欄と、ボタンが2つと、入力欄の横のテキスト(スタティックテキストと呼びます)があることが確認できます。UIでやると手数が増えることもあるということですね。このようにUIを使いこなす手順と、リソースをコードで直接編集する手順を知っていると楽ができます。ただし、リソースのコードがVisual Studioが読み取れないほどにルールに反していると二度とリソースをコンパイルすることも、エディタでみることもできなくなりますので、深い理解も必要です。
 更に、コードを変更していきます。メニューから飛んできたIDM_URLを受け取ったら、ダイアログを表示するようにコードを追加します。
<syntaxhighlight lang="cpp">
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;
    }
}
</syntaxhighlight>
 このようにDialogBox関数でリソースで定義したID_DIALOG_URLというダイアログが呼び出せます。ダイアログ用のプロシージャ関数を定義しましたので、
<syntaxhighlight lang="cpp">
BOOL CALLBACK UrlDialogProc(HWND, UINT, WPARAM, LPARAM);
</syntaxhighlight>
というプロトタイプ宣言や
<syntaxhighlight lang="cpp">
BOOL CALLBACK UrlDialogProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp){
switch (msg) {
    …
    }
}
</syntaxhighlight>
のようなダイアログメッセージプロシージャ関数本体が必要です。そうして、インターネット接続をして、urlが示すアドレスにhttp応答を呼び掛けて、受け取るという処理を行います。
<syntaxhighlight lang="cpp">
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;
}
</syntaxhighlight>
 と上記のように、pchStrUrlという文字列グローバル変数(これは暫定的な簡単なやり方です。文字列グローバル変数をWin32/64でうまく管理する記事はこちらに記述します。)にURLの文字列をダイアログの接続ボタンを押した後に受け取るようにします。
 ダイアログにあるテキストエディットボックスと文字列をやり取りするのは以下のような関数を使います。
=== SetDlgItemText ===
<span style="color:darkred;">'''■SetDlgItemText(HWND hDlg, int IDC_URL, LPWSTR pchStrUrl)'''</span>
'''第1引数:''' HWND hDlg
 ダイアログボックスのウィンドウハンドルです。通常はダイアログメッセージプロシージャ関数の第1引数で受け取ったものです。
'''第2引数:''' int IDC_URL
 ダイアログボックスにあるコントロールのidに割り当てられている数値です。
'''第3引数:''' LPWSTR pwchStrUrl or LPSTR pchStrUrl
 ダイアログボックスの指定のテキストエディットボックスに設定するワイド文字列かマルチバイト文字列変数です。
=== GetDlgItemText ===
<span style="color:darkred;">'''■GetDlgItemText(hDlg, IDC_URL, pchStrUrl, (int)sizeof(pchStrUrl));'''</span>
'''第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型の変数です。
<syntaxhighlight lang="cpp" line start=1>
HGLOBAL HGLOBALwpage;
wchar_t* pwchpage;
HGLOBAL HGLOBALwheader;
wchar_t* pwchheader;
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);
    }
    //マルチバイトUTF-8をwchar型に変換してHGLOBAL変数にhttp応答の内容を取得
    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)
    //マルチバイトUTF-8をwchar型に変換してHGLOBAL変数にhttpheader応答の内容を取得
    LPVOID LPVOIDBuffer;
    DWORD DWORDsize;
    HttpQueryInfo(HINTERNETurl, HTTP_QUERY_RAW_HEADERS_CRLF, NULL, &DWORDsize, NULL);
    HGLOBAL HGLOBALhead;
    HGLOBALhead = GlobalAlloc(GHND, DWORDsize);
    LPVOIDBuffer = (LPVOID)GlobalLock(HGLOBALhead);
    HttpQueryInfo(HINTERNETurl, HTTP_QUERY_RAW_HEADERS_CRLF, (LPVOID)LPVOIDBuffer, &DWORDsize, NULL);
   
    HGLOBALwheader = GlobalAlloc(GHND, ((SIZE_T)DWORDsize * sizeof(wchar_t)) + 1);
    pwchheader = (wchar_t*)GlobalLock(HGLOBALwheader);
    int iHeadLen = MultiByteToWideChar(CP-UTF8, 0, pwchheader, -1, NULL , 0);
    MultiByteToWideChar(CP-UTF8, 0, (LPSTR)LPVOIDBuffer, -1, pwchheader , iHeadLen);
    (wchar_t*)GlobalUnlock(HGLOBALwheader);
    //インターネットハンドル値を解放
InternetCloseHandle(HINTERNETurl);
InternetCloseHandle(HINTERNETinet);
}
</syntaxhighlight>
 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 =====
<span style="color:darkred;font-weight:bold">■InternetOpen(LPCWSTR, DWORD, LPCWSTR, LPCWSTR, DWORD)</span>
'''第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ハンドルを返します。
<span style="color:darkred;font-weight:bold">■InternetOpenUrl(HINTERNET, LPCWSTR, LPCWSTR, DWORD, DWORD, DWORD)</span>
'''第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 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と設定しておいてもいいわけです。
<span style="color:darkred;font-weight:bold">■InternetReadFile(HINTERNET, LPVOID, DWORD, LPDWORD)</span>
'''第1引数:'''HINTERNET hFile
 InternetOpenUrl関数を実行して得られたインターネットセッションへのハンドル。
'''第2引数:'''LPVOID lpBuffer
 文字列変数のポインタようなものを設定します。
'''第3引数:'''DWORD dwNumberOfBytesToRead
 第2引数に設定した変数のメモリ確保サイズを設定します。ここで設定したサイズだけ第2引数で設定した領域にデータを読み取り進めて、途中で合った場合は読み取り進めた位置を保持します。もう一度この関数を呼び出した時は、読み取り進めた位置から再度読み取りが始まります。また最初から読み出したい場合はDWORD newPosition = 0;InternetSetFilePointer(HINTERNETurl, newPosition, NULL, FILE_BEGIN, 0);のようにポジションを設定しなおす関数で実行できます。FILE_BEGINのとこは起点基準位置を差すフラグでFILE_CURRENTやFILE_ENDを使うこともできます。最後の0は予約されている引数で必ず0である必要があるとされています。第二引数は下位32ビットのLONG型の値ですが、第三引数は上位32ビットの値をさすLONG型のポインタです。4Gbyteを超えるような巨大なファイルを操作する操作するときに必要になりますが、巨大なデータベースで使われることもあるかもしれませんが、通常は使用することにはならないです。
'''第4引数:'''LPDWORD lpdwNumberOfBytesRead
 DWORD型変数へのポインタです。この引数にしていした変数の実体に読み取ったバイト数が返却されます。サンプルプログラムでは1025バイトの変数の1024バイト分の文字列が格納され、最後の1バイトは\0を格納するために使われます。配列要素番号0~1023の1024バイトです。1025バイト目にあたる配列の要素1024には\0を自分で設定しないと、ヌル終端文字列としての役割が持てないので、文字列操作関数で扱うことが難しい状態のままになってしまいます。
<span style="color:darkred;font-weight:bold">■HttpQueryInfo(HINTERNET, DWORD, LPVOID, LPDWORD, LPDWORD)</span>
'''第1引数:'''HINTERNET hRequest
 InternetOpenUrl関数を実行して得られたインターネットセッションへのハンドル。
'''第2引数:'''DWORD dwInfoLevel
 取得する属性と要求を変更するフラグの組み合わせ。いろいろあります。[https://learn.microsoft.com/ja-jp/windows/win32/wininet/query-info-flags https://learn.microsoft.com/ja-jp/windows/win32/wininet/query-info-flags]に一覧があります。
'''第3引数:'''LPVOID lpBuffer
 LPVOID型の変数で文字列を取得するメモリへの先頭アドレスを指定。
'''第4引数:'''LPDWORD lpdwBufferLength
 返された文字列の長さ。第3引数のメモリ確保領域の大きさが不足している場合、要求された情報を受信するのに十分な大きさのバッファーのサイズ (バイト単位) が返されます。
'''第5引数:'''LPDWORD lpdwIndex
 同じ名前の複数のヘッダーを列挙するために使用される 0 から始まるヘッダー インデックスへのポインター。念のため1づつ増やして、ヘッダー応答があるか確かめるべきだそうです。エラーが返ってくるまで繰り返し実行するということですね。複数のヘッダーがある場合は稀れですが、あるにはあるそうです。
 ht<span>t</span>ps://wiki.yo-net.jp/のヘッダー応答は以下のようなものです。
<syntaxhighlight lang="text">
HTTP/1.1 200 OK
Date: Wed, 08 Nov 2023 22:00:00 GMT
Server: Apache
X-Content-Type-Options: nosniff
Content-language: ja
Vary: Accept-Encoding,Cookie,User-Agent
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: private, must-revalidate, max-age=0
Connection: close
X-Request-Id: ZUtOqs75uZ183vwwlQqszgAAAZk
Strict-Transport-Security: max-age=31536000
Last-Modified: Sat, 04 Nov 2023 14:54:34 GMT
Content-Length: 245374
Content-Type: text/html; charset=UTF-8
</syntaxhighlight>


 
 
前の記事:[[Win32/64ネットワーク開発 001 ATLを使ったブラウザ]]
次の記事:[[Win32/64ネットワーク開発 003 wininetを使ったftp通信]]


[[Win32/64 ネットワーク処理開発]]へ戻る。
[[Win32/64 ネットワーク処理開発]]へ戻る。

2023年11月29日 (水) 23:08時点における最新版

Win32/64 ネットワーク処理開発へ戻る。

前の記事:Win32/64ネットワーク開発 001 ATLを使ったブラウザ

次の記事:Win32/64ネットワーク開発 003 wininetを使ったftp通信

概要

 またまた、あまり使い道が思いつかないライブラリを使ってみます。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

■SetDlgItemText(HWND hDlg, int IDC_URL, LPWSTR pchStrUrl)

第1引数: HWND hDlg

 ダイアログボックスのウィンドウハンドルです。通常はダイアログメッセージプロシージャ関数の第1引数で受け取ったものです。


第2引数: int IDC_URL

 ダイアログボックスにあるコントロールのidに割り当てられている数値です。


第3引数: LPWSTR pwchStrUrl or LPSTR pchStrUrl

 ダイアログボックスの指定のテキストエディットボックスに設定するワイド文字列かマルチバイト文字列変数です。


GetDlgItemText

■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;
HGLOBAL HGLOBALwheader;
wchar_t* pwchheader;

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

    //マルチバイトUTF-8をwchar型に変換してHGLOBAL変数にhttp応答の内容を取得
    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)


    //マルチバイトUTF-8をwchar型に変換してHGLOBAL変数にhttpheader応答の内容を取得
    LPVOID LPVOIDBuffer;
    DWORD DWORDsize;
    HttpQueryInfo(HINTERNETurl, HTTP_QUERY_RAW_HEADERS_CRLF, NULL, &DWORDsize, NULL);
    HGLOBAL HGLOBALhead;
    HGLOBALhead = GlobalAlloc(GHND, DWORDsize);
    LPVOIDBuffer = (LPVOID)GlobalLock(HGLOBALhead);
    HttpQueryInfo(HINTERNETurl, HTTP_QUERY_RAW_HEADERS_CRLF, (LPVOID)LPVOIDBuffer, &DWORDsize, NULL);
    
    HGLOBALwheader = GlobalAlloc(GHND, ((SIZE_T)DWORDsize * sizeof(wchar_t)) + 1);
    pwchheader = (wchar_t*)GlobalLock(HGLOBALwheader);
    int iHeadLen = MultiByteToWideChar(CP-UTF8, 0, pwchheader, -1, NULL , 0);
    MultiByteToWideChar(CP-UTF8, 0, (LPSTR)LPVOIDBuffer, -1, pwchheader , iHeadLen);
    (wchar_t*)GlobalUnlock(HGLOBALwheader);

    //インターネットハンドル値を解放
	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

■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と設定しておいてもいいわけです。


■InternetReadFile(HINTERNET, LPVOID, DWORD, LPDWORD)

第1引数:HINTERNET hFile

 InternetOpenUrl関数を実行して得られたインターネットセッションへのハンドル。


第2引数:LPVOID lpBuffer

 文字列変数のポインタようなものを設定します。


第3引数:DWORD dwNumberOfBytesToRead

 第2引数に設定した変数のメモリ確保サイズを設定します。ここで設定したサイズだけ第2引数で設定した領域にデータを読み取り進めて、途中で合った場合は読み取り進めた位置を保持します。もう一度この関数を呼び出した時は、読み取り進めた位置から再度読み取りが始まります。また最初から読み出したい場合はDWORD newPosition = 0;InternetSetFilePointer(HINTERNETurl, newPosition, NULL, FILE_BEGIN, 0);のようにポジションを設定しなおす関数で実行できます。FILE_BEGINのとこは起点基準位置を差すフラグでFILE_CURRENTやFILE_ENDを使うこともできます。最後の0は予約されている引数で必ず0である必要があるとされています。第二引数は下位32ビットのLONG型の値ですが、第三引数は上位32ビットの値をさすLONG型のポインタです。4Gbyteを超えるような巨大なファイルを操作する操作するときに必要になりますが、巨大なデータベースで使われることもあるかもしれませんが、通常は使用することにはならないです。


第4引数:LPDWORD lpdwNumberOfBytesRead

 DWORD型変数へのポインタです。この引数にしていした変数の実体に読み取ったバイト数が返却されます。サンプルプログラムでは1025バイトの変数の1024バイト分の文字列が格納され、最後の1バイトは\0を格納するために使われます。配列要素番号0~1023の1024バイトです。1025バイト目にあたる配列の要素1024には\0を自分で設定しないと、ヌル終端文字列としての役割が持てないので、文字列操作関数で扱うことが難しい状態のままになってしまいます。


■HttpQueryInfo(HINTERNET, DWORD, LPVOID, LPDWORD, LPDWORD)

第1引数:HINTERNET hRequest

 InternetOpenUrl関数を実行して得られたインターネットセッションへのハンドル。


第2引数:DWORD dwInfoLevel

 取得する属性と要求を変更するフラグの組み合わせ。いろいろあります。https://learn.microsoft.com/ja-jp/windows/win32/wininet/query-info-flagsに一覧があります。


第3引数:LPVOID lpBuffer

 LPVOID型の変数で文字列を取得するメモリへの先頭アドレスを指定。


第4引数:LPDWORD lpdwBufferLength

 返された文字列の長さ。第3引数のメモリ確保領域の大きさが不足している場合、要求された情報を受信するのに十分な大きさのバッファーのサイズ (バイト単位) が返されます。


第5引数:LPDWORD lpdwIndex

 同じ名前の複数のヘッダーを列挙するために使用される 0 から始まるヘッダー インデックスへのポインター。念のため1づつ増やして、ヘッダー応答があるか確かめるべきだそうです。エラーが返ってくるまで繰り返し実行するということですね。複数のヘッダーがある場合は稀れですが、あるにはあるそうです。


 https://wiki.yo-net.jp/のヘッダー応答は以下のようなものです。


HTTP/1.1 200 OK
Date: Wed, 08 Nov 2023 22:00:00 GMT
Server: Apache
X-Content-Type-Options: nosniff
Content-language: ja
Vary: Accept-Encoding,Cookie,User-Agent
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: private, must-revalidate, max-age=0
Connection: close
X-Request-Id: ZUtOqs75uZ183vwwlQqszgAAAZk
Strict-Transport-Security: max-age=31536000
Last-Modified: Sat, 04 Nov 2023 14:54:34 GMT
Content-Length: 245374
Content-Type: text/html; charset=UTF-8

 

前の記事:Win32/64ネットワーク開発 001 ATLを使ったブラウザ

次の記事:Win32/64ネットワーク開発 003 wininetを使ったftp通信

Win32/64 ネットワーク処理開発へ戻る。