Аварийный режим 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 - закрытие дескриптора, кажется, единственный способ прервать.

Действительно, это сработало. Но тут начинаются все проблемы ...

Первый неприятный сюрприз, с которым я столкнулся, заключается в том, что 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));
}

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

Access violation reading location 0xfeeefeee.

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

5
задан valdo 6 August 2010 в 08:24
поделиться