Извините за такой длинный вопрос.Просто я потратил несколько дней, пытаясь решить мою проблему, и я устал.
Я пытаюсь использовать 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 просто нельзя безопасно использовать?
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
(т. Е. Имеет виртуальные функции) - он устанавливается по указанному выше адресу.
Так что это попытка вызвать виртуальную функцию уже удаленного объекта.