抱歉问了这么长的问题。只是我花了几天时间试图解决我的问题,我已经筋疲力尽了。
我正在尝试在异步模式下使用 WinINet。我必须说……这简直是疯了。我真的无法理解这一点。它做了很多事情,但不幸的是,它的异步 API 设计得非常糟糕,以至于它不能用于具有高稳定性要求的严肃应用程序中。
我的问题如下:我需要连续执行大量 HTTP/HTTPS 事务,而我还需要能够在请求时立即中止它们。
我打算按以下方式使用 WinINet:
InternetOpen
通过带有INTERNET_FLAG_ASYNC
标志的函数初始化 WInINet 的使用。- 安装一个全局回调函数(通过
InternetSetStatusCallback
)。
现在,为了执行我想做的交易:
- 分配每个事务结构,其中包含描述事务状态的各种成员。
- 调用
InternetOpenUrl
以启动交易。在异步模式下,它通常会立即返回一个错误,即ERROR_IO_PENDING
. 它的参数之一是“上下文”,将传递给回调函数的值。我们将其设置为指向每个事务状态结构的指针。 - 在此之后不久,全局回调函数被调用(从另一个线程) status
INTERNET_STATUS_HANDLE_CREATED
。此时我们保存 WinINet 会话句柄。 INTERNET_STATUS_REQUEST_COMPLETE
当事务完成时,最终调用回调函数。这允许我们使用一些通知机制(例如设置事件)来通知发起线程事务已完成。- 发出事务的线程意识到它已经完成。然后它进行清理:关闭 WinINet 会话句柄 (by
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)编写的代码有特殊的意义。AFAIK,当您删除具有vtable
(即-具有虚拟功能)的对象时-将其设置为上述地址。所以这是尝试调用已删除对象的虚函数。