Извините за такой длинный вопрос. Просто я потратил несколько дней, пытаясь решить свою проблему, и я измотан.
Я пытаюсь использовать WinINet в асинхронном режиме. И я должен сказать... это просто безумие. Я действительно не могу этого понять. Он делает так много всего, но, к сожалению, его асинхронный API настолько плохо разработан, что его просто нельзя использовать в серьезном приложении с высокими требованиями к стабильности.
Моя проблема заключается в следующем: мне нужно выполнять много транзакций HTTP/HTTPS последовательно, тогда как мне также нужно иметь возможность немедленно прервать их по запросу.
Я собирался использовать WinINet следующим образом:
- Инициализировать использование WInINet с помощью функции
InternetOpen
с флагомINTERNET_FLAG_ASYNC
. - Установите глобальную функцию обратного вызова (через
InternetSetStatusCallback
).
Теперь, чтобы выполнить транзакцию, я подумал сделать:
- Выделите структуру для каждой транзакции с различными элементами, описывающими состояние транзакции.
- Позвоните
InternetOpenUrl
, чтобы инициировать транзакцию. В асинхронном режиме обычно сразу возвращается с ошибкойERROR_IO_PENDING
. Одним из его параметров является «контекст», значение, которое будет передано в функцию обратного вызова. Мы устанавливаем его в указатель на структуру состояния каждой транзакции. - Вскоре после этого вызывается глобальная функция обратного вызова (из другого потока) со статусом
INTERNET_STATUS_HANDLE_CREATED
. В этот момент мы сохраняем дескриптор сеанса WinINet. - В конце концов, функция обратного вызова вызывается с помощью
INTERNET_STATUS_REQUEST_COMPLETE
, когда транзакция завершена. Это позволяет нам использовать некоторый механизм уведомления (например, установку события), чтобы уведомить исходный поток о завершении транзакции. - Поток, выпустивший транзакцию, понимает, что она завершена. Затем он выполняет очистку: закрывает дескриптор сеанса 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
(т.е. имеет виртуальные функции), он устанавливается на указанный выше адрес. Так что это попытка вызвать виртуальную функцию уже удаленного объекта.