WinINet асинхронный режим бедствия - PullRequest
5 голосов
/ 05 августа 2010

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

Я пытаюсь использовать 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 просто нельзя безопасно использовать?

EDIT:

Это минимальный блок кода, который демонстрирует 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));
}

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

Access violation reading location 0xfeeefeee.

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

Ответы [ 2 ]

7 голосов
/ 02 марта 2011

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

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

2 голосов
/ 07 августа 2010

Особая благодарность Люку.

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

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

...