回调中的 WinHTTP 异步上下文无效

WinHTTP async context invalid in callback

我正在尝试使用 WinHTTP 对服务器进行异步调用,但遇到问题:

  1. 在转换和访问请求 ID 等结构值时,回调中的上下文似乎不正确。
  2. WinHttpReceiveResponse 总是 returns 使用 GetLastError() 的错误代码 6 这是无效句柄的系统错误,我怀疑它与第 1 点有关。

我能想到的上下文不再有效的唯一原因是它是否超出了范围,但我看不出这是怎么发生的。任何见解将不胜感激。

我的上下文结构是

struct connection_context
{
    int request_id;
    HANDLE read_complete;
    DWORD timeout;
    HINTERNET request_handle;
    std::string request_payload;
    std::string read_buffer;
    std::string response_payload;
};

我的 WinHTTP 回调是

void async_callback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
    if (dwContext != 0)
    {
        connection_context* ctx = (connection_context*)dwContext;

        switch(dwInternetStatus)
        {
            case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
                if(!WinHttpReceiveResponse(ctx->request_handle, NULL))
                {
                    printf("Request %d Error in receive response, code: %d\n", ctx->request_id, GetLastError());
                    SetEvent(ctx->read_complete);
                }
                break;
            case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
            {
                DWORD statusCode = 0;
                DWORD statusCodeSize = sizeof(DWORD);

                if (!WinHttpQueryHeaders(ctx->request_handle, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
                {
                    printf("Request %d failed to query headers\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }

                if (HTTP_STATUS_OK != statusCode)
                {
                    printf("Request %d bad status\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }

                if(!WinHttpQueryDataAvailable(ctx->request_handle, NULL))
                {
                    printf("Reques %d bad query data available\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }
                break;
            }
            case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
                if (*((LPDWORD)lpvStatusInformation) != 0)
                {
                    DWORD buffer_size = *((LPDWORD)lpvStatusInformation) + 1;
                    ctx->read_buffer = std::string(buffer_size, '[=11=]');

                    if (!WinHttpReadData(ctx->request_handle, &ctx->read_buffer[0], ctx->read_buffer.size(), 0))
                    {
                        printf("Request %d bad subsequent read data\n", ctx->request_id);
                        SetEvent(ctx->read_complete);
                    }
                }
                else
                {
                    printf("Request %d read complete with size %zd\n", ctx->request_id, ctx->response_payload.size());
                    SetEvent(ctx->read_complete);
                }
                break;
            case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
                if (dwStatusInformationLength != 0)
                {
                    ctx->response_payload.append(ctx->read_buffer.c_str(), dwStatusInformationLength);
                    ctx->read_buffer.clear();

                    if(!WinHttpQueryDataAvailable(ctx->request_handle, NULL))
                    {
                        printf("Request %d bad query data available\n", ctx->request_id);
                        SetEvent(ctx->read_complete);
                    }
                }
                break;        
        }
    }
}

建立连接、发送请求并等待它们完成的函数是

void fetch_status(std::string login_result, LPCWSTR pswzServerName, WORD port, LPCWSTR lpszVersion, LPCWSTR lpszReferrer, LPCWSTR* lplpszAcceptTypes, DWORD dwFlags, std::unordered_map<int, std::string>* results)
{
    HINTERNET session = WinHttpOpen(L"Steam", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);

    if (!session)
    {
        printf("Failed to make session\n");
        return;
    }

    WinHttpSetStatusCallback(session, async_callback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0);

    HINTERNET connect = WinHttpConnect(session, pswzServerName, port, 0);

    if (!connect)
    {
        printf("Failed to make connect\n");
        WinHttpCloseHandle(session);
        return;
    }

    std::vector<connection_context> contexts;

    std::string token = login_result.substr(60, 19) + login_result.substr(2, 14);
    token = to_hex(token);

    for (auto i = 0; i < ggs_get_aobs.size(); i++)
    {
        connection_context ctx;
        contexts.push_back(ctx);
        contexts.back().request_id = i;
        contexts.back().timeout = 5000; // 5 seconds
        contexts.back().request_payload = generate_stat_get_request(token, ggs_get_aobs[i]);
        contexts.back().request_handle = WinHttpOpenRequest(connect, L"POST", L"/api/statistics/get", NULL, WINHTTP_NO_REFERER, lplpszAcceptTypes, WINHTTP_FLAG_SECURE);

        if (!contexts.back().request_handle)
        {
            printf("Failed to open request\n");
            continue;
        }

        int headers_added = 0;

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Connection: keep-alive\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Content-Type: application/x-www-form-urlencoded\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Cache-Control: no-cache\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Cookie: theme=theme-dark\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if (WinHttpAddRequestHeaders(contexts.back().request_handle, L"User-Agent: Steam\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if (headers_added < 5)
        {
            printf("Failed to add headers\n");
            continue;
        }

        if(!WinHttpSendRequest(contexts.back().request_handle, WINHTTP_NO_ADDITIONAL_HEADERS, -1L, 
            &contexts.back().request_payload[0], contexts.back().request_payload.size() + 1, contexts.back().request_payload.size() + 1, (DWORD_PTR)&contexts.back()))
        {
            printf("Failed to send request\n");
            continue;
        }

        printf("Sent async http request %d, otional(size:%zd)\n", i, contexts.back().request_payload.size() + 1);
    }

    for (auto i = 0; i < contexts.size(); i++)
    {
        WaitForSingleObject(contexts[i].read_complete, contexts[i].timeout);
        WinHttpCloseHandle(contexts[i].request_handle);
    }

    WinHttpCloseHandle(connect);
    WinHttpCloseHandle(session);
}

在调用 WinHttpSendRequest() 的循环的每次迭代中,设置每个请求的上下文,对 contexts.push_back() 的调用有可能重新分配向量的内部数组,从而使之前的所有 connection_context* 您在之前的请求中使用的指针。

为避免重新分配,您需要在进入循环之前预先调整数组的大小。

要么像这样:

std::vector<connection_context> contexts(ggs_get_aobs.size());

...

size_t i = 0;
for (auto &ctx : contexts)
{
    ctx.request_id = i;
    ...
    ctx.request_handle = WinHttpOpenRequest(...);
    ...
    WinHttpSendRequest(ctx.request_handle, ..., reinterpret_cast<DWORD_PTR>(&ctx))
    ...
    ++i;
}

或者,像这样:

std::vector<connection_context> contexts;
contexts.reserve(ggs_get_aobs.size());

...

for (auto i = 0; i < ggs_get_aobs.size(); ++i)
{
    contexts.emplace_back();
    auto &ctx = contexts.back();
    /* or, in C++17 and later...
    auto &ctx = contexts.emplace_back();
    */

    ctx.request_id = i;
    ...
    ctx.request_handle = WinHttpOpenRequest(...);
    ...
    WinHttpSendRequest(ctx.request_handle, ..., reinterpret_cast<DWORD_PTR>(&ctx))
    ...
}