長い質問で申し訳ありません。問題を解決するために数日を費やしただけで、疲れ果てています。
WinINet を非同期モードで使用しようとしています。そして、私は言わなければなりません...これは単に正気ではありません. これは本当に理解できません。非常に多くのことを行いますが、残念ながらその非同期 API の設計は非常に貧弱であるため、高い安定性が要求される本格的なアプリケーションでは使用できません。
私の問題は次のとおりです。多くの HTTP/HTTPS トランザクションを連続して実行する必要がありますが、要求に応じてすぐに中止できる必要もあります。
次の方法で WinINet を使用するつもりでした。
InternetOpen
フラグ付きの関数を介して、WININet の使用を初期化しINTERNET_FLAG_ASYNC
ます。- グローバル コールバック関数をインストールします ( 経由
InternetSetStatusCallback
)。
さて、私がやろうと思っていたトランザクションを実行するために:
- トランザクションの状態を説明するさまざまなメンバーを使用して、トランザクションごとの構造を割り当てます。
- 電話
InternetOpenUrl
して取引を開始します。非同期モードでは、通常、すぐにエラーが返されますERROR_IO_PENDING
。パラメータの 1 つは「コンテキスト」で、コールバック関数に渡される値です。トランザクションごとの状態構造体へのポインターに設定します。 - この直後にグローバル コールバック関数が (別のスレッドから) status で呼び出されます
INTERNET_STATUS_HANDLE_CREATED
。この時点で、WinINet セッション ハンドルを保存します。 INTERNET_STATUS_REQUEST_COMPLETE
トランザクションが完了すると、最終的にコールバック関数が呼び出されます。これにより、何らかの通知メカニズム (イベントの設定など) を使用して、元のスレッドにトランザクションが完了したことを通知できます。- トランザクションを発行したスレッドは、トランザクションが完了したことを認識します。次に、クリーンアップを行います。WinINet セッション ハンドルを (によって
InternetCloseHandle
) 閉じ、状態構造を削除します。
今のところ問題はないようです。
実行中のトランザクションを中止するには? 1 つの方法は、適切な WinINet ハンドルを閉じることです。また、WinINet には次のような機能がないためInternetAbortXXXX
、ハンドルを閉じることが中止する唯一の方法のようです。
確かにこれはうまくいきました。ERROR_INTERNET_OPERATION_CANCELLED
このようなトランザクションは、エラー コードですぐに完了します。しかし、ここからすべての問題が始まります...
私が遭遇した最初の不愉快な驚きは、WinINet が、トランザクションが既に中止された後でも、トランザクションのコールバック関数を呼び出す傾向があることです。MSDN によるとINTERNET_STATUS_HANDLE_CLOSING
、コールバック関数の最後の呼び出しです。しかし、それは嘘です。私が見ているのは、同じハンドルに対する結果通知が時々あるということです。INTERNET_STATUS_REQUEST_COMPLETE
また、トランザクション ハンドルを閉じる直前にコールバック関数を無効にしようとしましたが、これは役に立ちませんでした。WinINet のコールバック呼び出しメカニズムは非同期のようです。したがって、トランザクション ハンドルが閉じられた後でも、コールバック関数を呼び出すことができます。
これには問題があります。WinINetがコールバック関数を呼び出すことができる限り、トランザクション状態構造を解放することはできません。しかし、WinINet が親切にそう呼んでくれるかどうか、どうして私が知ることができるでしょうか? 私が見たところ、一貫性がありません。
それにもかかわらず、私はこれを回避しました。代わりに、割り当てられたトランザクション構造のグローバル マップ (もちろんクリティカル セクションによって保護されています) を保持しています。次に、コールバック関数内で、トランザクションが実際に存在することを確認し、コールバックの呼び出し中にトランザクションをロックします。
しかし、これまで解決できなかった別の問題を発見しました。トランザクションが開始された直後にトランザクションを中止すると発生します。
を呼び出すと、エラー コードInternetOpenUrl
が返されます。ERROR_IO_PENDING
次に、コールバック関数がINTERNET_STATUS_HANDLE_CREATED
通知で呼び出されるまで(通常は非常に短い)待ちます。次に、トランザクション ハンドルが保存されるので、ハンドル/リソース リークなしで中止する機会が得られ、先に進むことができます。
この瞬間の直後に中止を試みました。つまり、このハンドルを受け取ったらすぐに閉じてください。何が起こると思いますか?WinINet がクラッシュします。無効なメモリ アクセスです! これは、コールバック関数で行うこととは関係ありません。コールバック関数は呼び出されません。クラッシュは WinINet の奥深くにあります。
一方、次の通知(「名前の解決」など)を待つと、通常は機能します。しかし、時々クラッシュすることもあります!Sleep
ハンドルを取得してから閉じるまでに最低限のことをすると、問題は解消されるようです。しかし、明らかにこれは深刻な解決策として受け入れられません。
以上のことから、WinINet の設計が不十分であるという結論に達しました。
- 特定のセッション (トランザクション) に対するコールバック関数の呼び出しの範囲について厳密な定義はありません。
- WinINet ハンドルを閉じることができるタイミングについて、厳密な定義はありません。
- 誰が他に何を知っていますか?
私が間違っている?それは私が理解できないものですか?それとも、WinINet を安全に使用できないのでしょうか?
編集:
これは、2 番目の問題であるクラッシュを示す最小限のコード ブロックです。エラー処理などをすべて削除しました。
HINTERNET g_hINetGlobal;
struct Context
{
HINTERNET m_hSession;
HANDLE m_hEvent;
};
void CALLBACK INetCallback(HINTERNET hInternet, DWORD_PTR dwCtx, DWORD dwStatus, PVOID pInfo, DWORD dwInfo)
{
if (INTERNET_STATUS_HANDLE_CREATED == dwStatus)
{
Context* pCtx = (Context*) dwCtx;
ASSERT(pCtx && !pCtx->m_hSession);
INTERNET_ASYNC_RESULT* pRes = (INTERNET_ASYNC_RESULT*) pInfo;
ASSERT(pRes);
pCtx->m_hSession = (HINTERNET) pRes->dwResult;
VERIFY(SetEvent(pCtx->m_hEvent));
}
}
void FlirtWInet()
{
g_hINetGlobal = InternetOpen(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC);
ASSERT(g_hINetGlobal);
InternetSetStatusCallback(g_hINetGlobal, INetCallback);
for (int i = 0; i < 100; i++)
{
Context ctx;
ctx.m_hSession = NULL;
VERIFY(ctx.m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL));
HINTERNET hSession = InternetOpenUrl(
g_hINetGlobal,
_T("http://ww.google.com"),
NULL, 0,
INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD,
DWORD_PTR(&ctx));
if (hSession)
ctx.m_hSession = hSession;
else
{
ASSERT(ERROR_IO_PENDING == GetLastError());
WaitForSingleObject(ctx.m_hEvent, INFINITE);
ASSERT(ctx.m_hSession);
}
VERIFY(InternetCloseHandle(ctx.m_hSession));
VERIFY(CloseHandle(ctx.m_hEvent));
}
VERIFY(InternetCloseHandle(g_hINetGlobal));
}
通常、1 回目または 2 回目の反復でアプリケーションがクラッシュします。WinINet によって作成されたスレッドの 1 つがアクセス違反を生成します。
Access violation reading location 0xfeeefeee.
上記のアドレスは、C++ (少なくとも MSVC) で記述されたコードにとって特別な意味を持つことに注意してください。AFAIK を持つvtable
(つまり、仮想関数を持つ) オブジェクトを削除すると、上記のアドレスに設定されます。そのため、既に削除されたオブジェクトの仮想関数を呼び出そうとしています。