【发布时间】:2011-03-25 21:46:59
【问题描述】:
抱歉问了这么长的问题。只是我花了几天时间试图解决我的问题,我已经筋疲力尽了。
我正在尝试在异步模式下使用 WinINet。而且我必须说...这简直是疯狂。我真的无法理解这一点。它做了很多事情,但不幸的是,它的异步 API 设计得非常糟糕,以至于它不能用于具有高稳定性要求的严肃应用程序中。
我的问题如下:我需要串行执行大量 HTTP/HTTPS 事务,而我还需要能够在请求时立即中止它们。
我打算按以下方式使用 WinINet:
- 通过带有
INTERNET_FLAG_ASYNC标志的InternetOpen函数初始化WInINet 使用。 - 安装全局回调函数(通过
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 的对象(即 - 具有虚拟功能)时,AFAIK - 它被设置为上述地址。
所以这是试图调用已删除对象的虚函数。
【问题讨论】:
-
我同意它有点难以使用,但我没有看到同样的问题。对于您的第一个问题,您确定为 same 句柄发送 INTERNET_STATUS_HANDLE_CLOSING 和 INTERNET_STATUS_REQUEST_COMPLETE 吗?我在回调中得到了不同的句柄,但奇怪的是 REQUEST_COMPLETE 似乎给了我从 InternetOpen 获得的句柄。此外,您的第二种情况不会导致我崩溃。我正在使用 Windows XP SP3。
-
卢克,感谢您的回复。是的,INTERNET_STATUS_REQUEST_COMPLETE 用于另一个句柄。但它带有相同的“上下文”值,我用它来识别我的请求状态。意味着 - 如果在我删除此状态后收到此通知 - 将会出现问题。但是,正如我所提到的,这可以解决。另一个问题似乎更加残酷。这种情况经常发生(尽管并非总是如此)。我说的是在 WinINet 报告会话句柄后立即关闭会话句柄。然后,在短时间内,WinINet 代码生成访问冲突。
-
我认为内部 InternetOpenUrl 等效于 InternetConnect + HttpOpenRequest(用于 http 资源),因此使用 Connect 句柄和 Request 句柄调用回调(取决于发生的特定事件)。由于 InternetOpenUrl 只接受一个上下文参数,因此它被传递给两个句柄。我已经尝试了很多次,但是在 HANDLE_CREATED 期间调用 InternetCloseHandle 时我无法让它崩溃。不知道可能是什么问题。
-
嗯...这很有趣。我将尝试使用 InternetConnect + HttpOpenRequest 而不是 InternetOpenUrl。这有点限制,因为现在我们只支持 http/https,但因为这是我真正需要的 - 值得一试。不过,我已经编辑了这个问题。它现在包含演示崩溃的代码
-
您的示例代码对我来说崩溃了;有趣的。似乎在尝试修改似乎已被删除的 Connection 对象时崩溃了。我想知道该对象是否没有用关键部分或其他东西正确保护。
标签: c++ c windows networking wininet