5

長い質問で申し訳ありません。問題を解決するために数日を費やしただけで、疲れ果てています。

WinINet を非同期モードで使用しようとしています。そして、私は言わなければなりません...これは単に正気ではありません. これは本当に理解できません。非常に多くのことを行いますが、残念ながらその非同期 API の設計は非常に貧弱であるため、高い安定性が要求される本格的なアプリケーションでは使用できません。

私の問題は次のとおりです。多くの HTTP/HTTPS トランザクションを連続して実行する必要がありますが、要求に応じてすぐに中止できる必要もあります。

次の方法で WinINet を使用するつもりでした。

  1. InternetOpenフラグ付きの関数を介して、WININet の使用を初期化しINTERNET_FLAG_ASYNCます。
  2. グローバル コールバック関数をインストールします ( 経由InternetSetStatusCallback)。

さて、私がやろうと思っていたトランザクションを実行するために:

  1. トランザクションの状態を説明するさまざまなメンバーを使用して、トランザクションごとの構造を割り当てます。
  2. 電話InternetOpenUrlして取引を開始します。非同期モードでは、通常、すぐにエラーが返されますERROR_IO_PENDING。パラメータの 1 つは「コンテキスト」で、コールバック関数に渡される値です。トランザクションごとの状態構造体へのポインターに設定します。
  3. この直後にグローバル コールバック関数が (別のスレッドから) status で呼び出されますINTERNET_STATUS_HANDLE_CREATED。この時点で、WinINet セッション ハンドルを保存します。
  4. INTERNET_STATUS_REQUEST_COMPLETEトランザクションが完了すると、最終的にコールバック関数が呼び出されます。これにより、何らかの通知メカニズム (イベントの設定など) を使用して、元のスレッドにトランザクションが完了したことを通知できます。
  5. トランザクションを発行したスレッドは、トランザクションが完了したことを認識します。次に、クリーンアップを行います。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(つまり、仮想関数を持つ) オブジェクトを削除すると、上記のアドレスに設定されます。そのため、既に削除されたオブジェクトの仮想関数を呼び出そうとしています。

4

2 に答える 2

8

Context ctxの宣言は問題の原因であり、for(;;)ループ内で宣言されているため、ループごとに作成されたローカル変数であり、破棄され、各ループの終了時にアクセスできなくなります。

その結果、コールバックが呼び出されたときに、ctxはすでに破棄されており、コールバックに渡されたポインターは破棄されたctxを指し、無効なメモリポインターがクラッシュを引き起こします。

于 2011-03-02T16:15:19.643 に答える
2

ルークに感謝します。

all-in-one の代わりにInternetConnect++を明示的に使用するHttpOpenRequestと、すべての問題がなくなります。HttpSendRequestInternetOpenUrl

リクエストハンドルで通知を受け取りません(「接続」ハンドルと混同しないでください)。さらに、クラッシュすることはもうありません。

于 2010-08-06T20:14:18.593 に答える