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