Сбой в асинхронном режиме WinINet

Извините за такой длинный вопрос. Просто я потратил несколько дней, пытаясь решить свою проблему, и я измотан.

Я пытаюсь использовать WinINet в асинхронном режиме. И я должен сказать... это просто безумие. Я действительно не могу этого понять. Он делает так много всего, но, к сожалению, его асинхронный API настолько плохо разработан, что его просто нельзя использовать в серьезном приложении с высокими требованиями к стабильности.

Моя проблема заключается в следующем: мне нужно выполнять много транзакций HTTP/HTTPS последовательно, тогда как мне также нужно иметь возможность немедленно прервать их по запросу.

Я собирался использовать WinINet следующим образом:

  1. Инициализировать использование WInINet с помощью функции InternetOpen с флагом INTERNET_FLAG_ASYNC.
  2. Установите глобальную функцию обратного вызова (через InternetSetStatusCallback).

Теперь, чтобы выполнить транзакцию, я подумал сделать:

  1. Выделите структуру для каждой транзакции с различными элементами, описывающими состояние транзакции.
  2. Позвоните InternetOpenUrl, чтобы инициировать транзакцию. В асинхронном режиме обычно сразу возвращается с ошибкой ERROR_IO_PENDING. Одним из его параметров является «контекст», значение, которое будет передано в функцию обратного вызова. Мы устанавливаем его в указатель на структуру состояния каждой транзакции.
  3. Вскоре после этого вызывается глобальная функция обратного вызова (из другого потока) со статусом INTERNET_STATUS_HANDLE_CREATED. В этот момент мы сохраняем дескриптор сеанса WinINet.
  4. В конце концов, функция обратного вызова вызывается с помощью INTERNET_STATUS_REQUEST_COMPLETE, когда транзакция завершена. Это позволяет нам использовать некоторый механизм уведомления (например, установку события), чтобы уведомить исходный поток о завершении транзакции.
  5. Поток, выпустивший транзакцию, понимает, что она завершена. Затем он выполняет очистку: закрывает дескриптор сеанса WinINet (на InternetCloseHandle) и удаляет структуру состояния.

Пока вроде проблем нет.

Как прервать транзакцию, которая находится в процессе выполнения? Один из способов — закрыть соответствующий дескриптор 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 просто нельзя безопасно использовать?

ИЗМЕНИТЬ:

Это минимальный блок кода, демонстрирующий вторую проблему: сбой. Я удалил всю обработку ошибок и т.д.

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

Обычно на первой/второй итерации приложение падает. Один из потоков, созданных WinINet, генерирует нарушение прав доступа:

Access violation reading location 0xfeeefeee.

Стоит отметить, что указанный выше адрес имеет особое значение для кода, написанного на C++ (по крайней мере, MSVC). Насколько я знаю, когда вы удаляете объект, который имеет vtable (т.е. имеет виртуальные функции), он устанавливается на указанный выше адрес. Так что это попытка вызвать виртуальную функцию уже удаленного объекта.


person valdo    schedule 05.08.2010    source источник
comment
Я согласен, что это немного сложно использовать, но я не вижу тех же проблем. Что касается вашей первой проблемы, вы уверены, что INTERNET_STATUS_HANDLE_CLOSING и INTERNET_STATUS_REQUEST_COMPLETE отправляются для одного и того же дескриптора? Я получаю разные дескрипторы в своем обратном вызове, хотя, как ни странно, REQUEST_COMPLETE, похоже, дает мне дескриптор, который я получаю от InternetOpen. Кроме того, ваш второй сценарий не вызывает у меня сбоя. Я использую Windows XP SP3.   -  person Luke    schedule 06.08.2010
comment
Люк, спасибо за ответ. Да, INTERNET_STATUS_REQUEST_COMPLETE приходит для другого дескриптора. Но он поставляется с тем же значением «контекст», которое я использую для идентификации состояния моего запроса. Значит - если это уведомление придет после того, как я удалю это состояние - будет проблема. Но, как я уже говорил, это можно обойти. Другая проблема кажется гораздо более жестокой. И это случается часто (хотя и не всегда). Я говорю о закрытии дескриптора сеанса немедленно после того, как WinINet сообщил об этом. Затем, через короткий промежуток времени, код WinINet генерирует нарушение прав доступа.   -  person valdo    schedule 06.08.2010
comment
Я думаю, что внутренне InternetOpenUrl эквивалентен InternetConnect + HttpOpenRequest (для ресурсов http), поэтому обратный вызов вызывается как с дескриптором подключения, так и с дескриптором запроса (в зависимости от того, какие конкретные события происходят). Поскольку InternetOpenUrl принимает только один параметр контекста, он передается для обоих дескрипторов. Я пробовал много раз, но не могу добиться сбоя при вызове InternetCloseHandle во время HANDLE_CREATED. Не знаю, в чем может быть проблема.   -  person Luke    schedule 06.08.2010
comment
Хм... это интересно. Я попробую использовать InternetConnect + HttpOpenRequest вместо InternetOpenUrl. Это немного ограничивает, так как теперь мы поддерживаем только http/https, но поскольку это то, что мне действительно нужно, стоит попробовать. Тем не менее я отредактировал вопрос. Теперь он включает код, демонстрирующий сбой   -  person valdo    schedule 06.08.2010
comment
Ваш пример кода дает сбой для меня; интересный. Похоже, происходит сбой при попытке изменить объект Connection, который, по-видимому, был удален. Интересно, этот объект не защищен должным образом критическим разделом или чем-то еще?   -  person Luke    schedule 08.08.2010
comment
Возможно. Тем не менее, теперь я изменил код, чтобы он делал то, что вы предложили, и проблем нет. Спасибо. P.S. Я очень хотел проголосовать за ваш пост, но так как это был комментарий - не смог.   -  person valdo    schedule 08.08.2010
comment
Я не могу заставить код примера сбой даже после 100000 итераций. У меня Win 7 SP1 64-битная, но компилирую код для 32-битной.   -  person James Johnston    schedule 07.03.2016


Ответы (2)


объявление контекста ctx является источником проблемы, оно объявлено в цикле for(;;), поэтому это локальная переменная, созданная для каждого цикла, она будет уничтожена и больше не будет доступна в конце каждого цикла.

в результате, когда вызывается обратный вызов, ctx уже уничтожен, указатель, передаваемый в обратный вызов, указывает на уничтоженный ctx, неверный указатель памяти вызывает сбой.

person zjp    schedule 02.03.2011

Отдельное спасибо Люку.

Все проблемы исчезают, когда я явно использую InternetConnect + HttpOpenRequest + HttpSendRequest вместо все-в-одном InternetOpenUrl.

Я не получаю никаких уведомлений по дескриптору request (не путать с дескриптором «соединения»). Плюс больше никаких сбоев.

person valdo    schedule 06.08.2010
comment
Я настоятельно рекомендую вам рассмотреть то, что предложил zjp. Возможно, ваш новый код был изменен, чтобы эта проблема больше не возникала, но вы использовали переменную, выходящую за рамки. - person André Caron; 02.03.2011