【问题标题】:WinINet asynchronous mode disasterWinINet 异步模式灾难
【发布时间】:2011-03-25 21:46:59
【问题描述】:

抱歉问了这么长的问题。只是我花了几天时间试图解决我的问题,我已经筋疲力尽了。

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

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

我打算按以下方式使用 WinINet:

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

现在,为了执行我想做的交易:

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


【解决方案1】:

特别感谢卢克。

当我明确使用 InternetConnect + HttpOpenRequest + HttpSendRequest 而不是一体式 InternetOpenUrl 时,所有问题都消失了。

我没有收到关于 request 句柄的任何通知(不要与“连接”句柄混淆)。此外,不再发生崩溃。

【讨论】:

  • 我强烈建议您考虑一下 zjp 的建议。您的新代码可能已更改,因此不再出现此问题,但您使用了超出范围的变量。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2012-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-04
    • 2010-10-14
    • 2014-11-18
    • 1970-01-01
    • 2012-03-16
    相关资源
    最近更新 更多