WinINet异步模式灾难

val*_*ldo 5 c c++ windows networking wininet

对不起这么长的问题.我只是花了好几天试图解决我的问题,而且我已经筋疲力尽了.

我正在尝试以异步模式使用WinINet.我必须说......这简直就是疯了.我真的无法理解这一点.它做了很多事情,但遗憾的是它的异步API设计得非常糟糕,以至于它不能用于具有高稳定性要求的严肃应用程序.

我的问题如下:我需要连续进行大量的HTTP/HTTPS事务,而我还需要能够在请求时立即中止它们.

我将按照以下方式使用WinINet:

  1. 通过InternetOpen带有INTERNET_FLAG_ASYNC标志的函数初始化WInINet用法.
  2. 安装全局回调函数(via InternetSetStatusCallback).

现在,为了执行我想做的事务:

  1. 使用描述事务状态的各种成员分配每事务结构.
  2. 致电InternetOpenUrl发起交易.在异步模式下,它通常会立即返回错误,即ERROR_IO_PENDING.其中一个参数是'context',它将传递给回调函数.我们将其设置为指向每个事务状态结构的指针.
  3. 在此之后不久,将调用具有status的全局回调函数(来自另一个线程)INTERNET_STATUS_HANDLE_CREATED.此时我们保存WinINet会话句柄.
  4. 最终INTERNET_STATUS_REQUEST_COMPLETE在事务完成时调用回调函数.这允许我们使用一些通知机制(例如设置事件)来通知原始线程事务已完成.
  5. 发出事务的线程意识到它已完成.然后它执行清理:关闭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));
}
Run Code Online (Sandbox Code Playgroud)

通常在第一次/第二次迭代时,应用程序崩溃.WinINet创建的线程之一会生成访问冲突:

Access violation reading location 0xfeeefeee.
Run Code Online (Sandbox Code Playgroud)

值得注意的是,上述地址对用C++编写的代码(至少是MSVC)有特殊意义.当您删除具有vtable(即具有虚函数)的对象时AFAIK - 它被设置为上述地址.因此,它试图调用已删除对象的虚函数.

小智 7

Context ctx的声明是问题的根源,它在for(;;)循环中声明,因此它是为每个循环创建的局部变量,它将被销毁,并且在每个循环结束时不再可访问.

因此,当调用回调时,ctx已经被销毁,指针被传递给回调点指向被破坏的ctx,无效的内存指针会导致崩溃.


val*_*ldo 2

特别感谢卢克。

InternetConnect当我明确使用+ HttpOpenRequest+HttpSendRequest而不是 all-in-one时,所有问题都消失了InternetOpenUrl

我没有收到有关请求句柄的任何通知(不要与“连接”句柄混淆)。而且不再发生崩溃。

  • 我强烈建议你考虑zjp的建议。您的新代码可能已更改,因此该问题不再发生,但您“正在”使用超出范围的变量。 (2认同)