C语言如何用WinHTTP发送HTTP请求?

99ANYc3cd6
预计阅读时长 34 分钟
位置: 首页 C语言 正文

WinHTTP (Windows HTTP Services) 是一个 Windows 平台下的 API,专门用于在应用程序中发起 HTTP/HTTPS 请求,它被设计成一个会话感知、非阻塞的 API,非常适合在服务(如 Windows 服务)、后台进程或需要高性能网络通信的桌面应用程序中使用。

c语言 winhttp
(图片来源网络,侵删)

与更通用的 WinINet API 相比,WinHTTP 的主要优势在于:

  • 服务端支持:可以在没有用户登录的 Windows 服务中运行。
  • 代理和系统配置:能更好地利用系统的代理设置(如通过 WinHTTP 代理自动发现 WPAD)。
  • 性能:针对高性能场景进行了优化。

核心概念和工作流程

使用 WinHTTP 的基本流程遵循一个经典的“打开-使用-关闭”模式,类似于文件操作:

  1. 初始化 (WinHttpInitialize):在使用任何 WinHTTP 函数之前,必须先调用此函数进行初始化。
  2. 会话 (WinHttpOpen):创建一个会话句柄,这个句柄代表了一个独立的 HTTP 会话上下文,后续的所有操作都基于此会话。
  3. 连接 (WinHttpConnect):使用会话句柄,连接到一个指定的 HTTP 服务器(主机名和端口)。
  4. 请求 (WinHttpOpenRequest):创建一个请求句柄,你需要指定请求的路径、方法(如 GET, POST)、HTTP 版本等。
  5. 发送请求 (WinHttpSendRequest):将请求头和可选的请求体(如 POST 数据)发送到服务器。
  6. 接收数据 (WinHttpReceiveResponseWinHttpReadData)
    • WinHttpReceiveResponse:等待并接收服务器的响应头。
    • WinHttpQueryDataAvailable:查询响应体中还有多少数据可以读取。
    • WinHttpReadData:循环读取响应体的数据。
  7. 清理 (WinHttpCloseHandleWinHttpCleanup)
    • WinHttpCloseHandle:关闭请求句柄和连接句柄。
    • WinHttpCleanup:释放 WinHTTP 库资源。

一个简单的 GET 请求示例

下面是一个完整的 C 语言示例,它向 httpbin.org/get 发送一个 GET 请求,并打印出响应内容。

#include <stdio.h>
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
int main() {
    // 1. 初始化 WinHTTP
    if (!WinHttpInitialize(WINHTTP_ACCESS_TYPE_DEFAULT_PROXY)) {
        printf("WinHttpInitialize failed. Error: %d\n", GetLastError());
        return 1;
    }
    // 2. 创建一个会话句柄
    HINTERNET hSession = WinHttpOpen(L"WinHTTP Example/1.0",
                                    WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                    WINHTTP_NO_PROXY_NAME,
                                    WINHTTP_NO_PROXY_BYPASS,
                                    0);
    if (!hSession) {
        printf("WinHttpOpen failed. Error: %d\n", GetLastError());
        WinHttpCleanup();
        return 1;
    }
    // 3. 连接到服务器
    // 使用 WinHttpConnect 时,URL 的协议部分 (http://) 必须省略
    HINTERNET hConnect = WinHttpConnect(hSession,
                                       L"httpbin.org", // 主机名
                                       INTERNET_DEFAULT_HTTP_PORT, // 端口
                                       0);
    if (!hConnect) {
        printf("WinHttpConnect failed. Error: %d\n", GetLastError());
        WinHttpCloseHandle(hSession);
        WinHttpCleanup();
        return 1;
    }
    // 4. 创建一个请求句柄
    // 路径是 /get
    HINTERNET hRequest = WinHttpOpenRequest(hConnect,
                                           L"GET", // 方法
                                           L"/get", // 路径
                                           NULL, // 版本 (NULL 表示使用默认)
                                           WINHTTP_NO_REFERER,
                                            WINHTTP_DEFAULT_ACCEPT_TYPES,
                                            WINHTTP_FLAG_SECURE); // 安全标志,即使是 HTTP 也可以加
    if (!hRequest) {
        printf("WinHttpOpenRequest failed. Error: %d\n", GetLastError());
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hSession);
        WinHttpCleanup();
        return 1;
    }
    // 5. 发送请求 (这里没有请求体,所以第三个参数为 0)
    if (!WinHttpSendRequest(hRequest,
                           WINHTTP_NO_ADDITIONAL_HEADERS,
                           0,
                           NULL,
                           0,
                           0,
                           0)) {
        printf("WinHttpSendRequest failed. Error: %d\n", GetLastError());
        WinHttpCloseHandle(hRequest);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hSession);
        WinHttpCleanup();
        return 1;
    }
    // 6. 接收响应
    if (!WinHttpReceiveResponse(hRequest, NULL)) {
        printf("WinHttpReceiveResponse failed. Error: %d\n", GetLastError());
        WinHttpCloseHandle(hRequest);
        WinHttpCloseHandle(hConnect);
        WinHttpCloseHandle(hSession);
        WinHttpCleanup();
        return 1;
    }
    // 7. 读取响应数据
    DWORD dwSize = 0;
    DWORD dwDownloaded = 0;
    char pBuffer[1024];
    // 查询数据可用性
    while (WinHttpQueryDataAvailable(hRequest, &dwSize)) {
        if (dwSize == 0) {
            printf("No data available.\n");
            break;
        }
        // 读取数据
        ZeroMemory(pBuffer, sizeof(pBuffer));
        if (!WinHttpReadData(hRequest, pBuffer, dwSize, &dwDownloaded)) {
            printf("Error %d in WinHttpReadData.\n", GetLastError());
            break;
        }
        // 打印读取到的数据
        printf("%.*s", dwDownloaded, pBuffer);
    }
    // 8. 清理资源
    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    WinHttpCleanup();
    return 0;
}

如何编译和运行:

c语言 winhttp
(图片来源网络,侵删)
  1. 将代码保存为 winhttp_get.c
  2. 使用 Visual Studio 的开发者命令提示符(或 MinGW)进行编译:
    cl winhttp_get.c /link winhttp.lib
  3. 运行生成的 winhttp_get.exe

一个 POST 请求示例

POST 请求与 GET 的主要区别在于需要发送请求体,关键步骤在 WinHttpSendRequestWinHttpWriteData

// ... (前面的初始化、会话、连接步骤与 GET 示例相同) ...
// 4. 创建一个请求句柄 (POST 请求)
HINTERNET hRequest = WinHttpOpenRequest(hConnect,
                                       L"POST", // 方法改为 POST
                                       L/post,  // 路径
                                       NULL,
                                       WINHTTP_NO_REFERER,
                                        WINHTTP_DEFAULT_ACCEPT_TYPES,
                                        WINHTTP_FLAG_SECURE);
// 5. 发送请求头
// 我们需要手动添加 Content-Type 头
LPCWSTR headers = L"Content-Type: application/x-www-form-urlencoded";
if (!WinHttpAddRequestHeaders(hRequest, headers, (ULONG)wcslen(headers), WINHTTP_ADDREQ_FLAG_ADD)) {
    printf("WinHttpAddRequestHeaders failed. Error: %d\n", GetLastError());
    // ... 清理 ...
    return 1;
}
// 准备要发送的 POST 数据
LPCWSTR postData = L"key1=value1&key2=value2";
DWORD postDataSize = (DWORD)(wcslen(postData) * sizeof(WCHAR)); // 注意这里是字节长度
// 6. 发送请求 (包含请求头和请求体长度)
if (!WinHttpSendRequest(hRequest,
                        WINHTTP_NO_ADDITIONAL_HEADERS, // 因为我们已经用 WinHttpAddRequestHeaders 添加了
                        0,
                        (LPVOID)postData, // 请求体数据
                        postDataSize,     // 请求体大小
                        postDataSize,     // 总发送大小
                        0)) {
    printf("WinHttpSendRequest failed. Error: %d\n", GetLastError());
    // ... 清理 ...
    return 1;
}
// 7. 发送请求体数据 (如果数据很大,需要循环发送)
// 在这个简单例子中,所有数据都在一个缓冲区里,所以这里不需要调用 WinHttpWriteData
// WinHttpSendRequest 的 lpOptional 参数已经处理了。
// 8. 接收响应和读取数据 (与 GET 示例相同)
if (!WinHttpReceiveResponse(hRequest, NULL)) {
    printf("WinHttpReceiveResponse failed. Error: %d\n", GetLastError());
    // ... 清理 ...
    return 1;
}
// ... (读取响应数据的代码与 GET 示例相同) ...
// 9. 清理资源 (与 GET 示例相同)

处理 HTTPS 和 SSL/TLS

WinHTTP 对 HTTPS 的支持非常简单,在 WinHttpOpenRequest 时,只需将最后一个 dwFlags 参数设置为 WINHTTP_FLAG_SECURE 即可。

HINTERNET hRequest = WinHttpOpenRequest(hConnect,
                                       L"GET",
                                       L"/api/secure-data",
                                       NULL,
                                       WINHTTP_NO_REFERER,
                                        WINHTTP_DEFAULT_ACCEPT_TYPES,
                                        WINHTTP_FLAG_SECURE); // 关键在这里

WinHTTP 会自动处理 SSL/TLS 握手,如果需要更高级的证书验证(忽略自签名证书的错误),可以使用 WinHttpSetOptionWINHTTP_OPTION_SECURITY_FLAGS

// 忽略证书错误 (仅用于测试或受信任的内网环境)
DWORD dwSecurityFlags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | SECURITY_FLAG_IGNORE_UNKNOWN_CA;
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwSecurityFlags, sizeof(dwSecurityFlags));

同步 vs. 异步操作

上面的例子都是同步的,即函数会阻塞,直到操作完成,对于桌面应用,这可能会导致界面卡顿。

c语言 winhttp
(图片来源网络,侵删)

WinHTTP 也支持异步操作,通过 WinHttpOpenRequest 时指定 WINHTTP_FLAG_ASYNC 标志,并使用回调函数来处理事件。

// 1. 在 WinHttpOpenRequest 时添加异步标志
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_ASYNC);
// 2. 设置回调函数
WinHttpSetStatusCallback(hRequest, (WINHTTP_STATUS_CALLBACK)MyCallback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0);
// 3. 发送请求后,程序不会阻塞,而是等待回调函数被调用
WinHttpSendRequest(hRequest, ...);
// 4. 回调函数原型
void CALLBACK MyCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) {
    switch (dwInternetStatus) {
        case WINHTTP_CALLBACK_STATUS_REQUEST_COMPLETE:
            // 请求完成,可以在这里开始读取数据
            break;
        case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
            // 数据可用,可以在这里调用 WinHttpReadData
            break;
        // ... 其他状态 ...
    }
}

异步模式更适合构建响应式的用户界面或需要处理多个网络连接的服务。


总结与最佳实践

  1. 错误处理:始终检查每个 WinHTTP 函数的返回值,并使用 GetLastError() 获取详细的错误信息。
  2. 资源管理:遵循“后创建先销毁”的原则,确保 WinHttpCloseHandleWinHttpCleanup 被正确调用,防止内存泄漏,可以使用 gotoif/else 块来简化清理逻辑。
  3. 字符编码:WinHTTP 内部使用宽字符(WCHAR),如果你的应用使用多字节字符(char),需要进行转换(使用 MultiByteToWideChar)。
  4. 性能:对于重复请求到同一服务器的场景,可以复用 hSessionhConnect 句柄,只重新创建 hRequest
  5. 超时:可以使用 WinHttpSetOption 来设置连接和请求的超时时间,防止程序长时间挂起。

希望这份详细的指南能帮助你在 C 语言项目中成功使用 WinHTTP!

-- 展开阅读全文 --
头像
ployfit c语言
« 上一篇 04-13
html5 织梦模板
下一篇 » 04-13

相关文章

取消
微信二维码
支付宝二维码

目录[+]