WinHttp 类封装:支持 GET、POST、多线程文件下载 🖥️📂
在现代应用开发中,网络请求是不可或缺的一部分。WinHttp 是 Windows 平台上提供的一套强大的 HTTP 服务接口,广泛用于实现客户端与服务器之间的通信。为了简化和优化网络请求操作,本文将详细介绍如何封装一个支持 GET、POST 请求以及 多线程文件下载 的 WinHttp 类。通过本教程,您将能够高效地进行网络编程,提高应用的性能和响应速度。
目录
WinHttp 简介
WinHttp(Windows HTTP Services)是微软为 Windows 平台提供的一套 HTTP 协议客户端 API。它支持多种 HTTP 操作,如发送请求、接收响应、处理重定向、管理连接等。相比于传统的 WinInet,WinHttp 更加轻量且适用于服务器端应用程序,具备更高的性能和更好的资源管理能力。
WinHttp 的主要功能包括:
- HTTP/HTTPS 请求:支持各种 HTTP 方法,如 GET、POST、PUT、DELETE 等。
- 代理支持:支持通过代理服务器进行请求。
- 异步操作:支持异步模式,提升应用的响应性。
- 多线程支持:适用于多线程环境,提升并发性能。
- 证书管理:支持 SSL/TLS 协议,提供安全通信。
WinHttp 封装类设计
为了简化 WinHttp 的使用,提升代码的可读性和可维护性,我们将封装一个 WinHttpClient
类。该类将提供易于调用的方法来执行 GET、POST 请求以及支持 多线程文件下载。
类设计要点
- 封装初始化与清理:在构造函数中初始化 WinHttp 会话,在析构函数中进行资源清理。
- 支持 GET 和 POST 请求:提供简洁的方法接口,允许用户轻松发送 GET 和 POST 请求。
- 多线程文件下载:利用多线程技术,支持将大文件分割成多个部分并行下载,提高下载速度。
- 错误处理:提供详细的错误信息,便于调试和维护。
- 灵活配置:允许用户自定义请求头、超时时间等参数。
类结构示意
class WinHttpClient {
public:
WinHttpClient();
~WinHttpClient();
// GET 请求
bool Get(const std::wstring& url, std::wstring& response);
// POST 请求
bool Post(const std::wstring& url, const std::wstring& data, std::wstring& response);
// 多线程文件下载
bool DownloadFile(const std::wstring& url, const std::wstring& savePath, int threadCount = 4);
private:
HINTERNET hSession;
HINTERNET hConnect;
bool Initialize();
void Cleanup();
// 其他辅助方法
};
解释:
- 构造函数与析构函数:负责初始化和清理 WinHttp 会话。
- Get 和 Post 方法:用于发送 GET 和 POST 请求,分别接收 URL、数据和响应内容。
- DownloadFile 方法:实现多线程文件下载,接收下载 URL、保存路径以及线程数量。
实现 GET 请求
GET 请求是最常见的 HTTP 请求类型,用于从服务器获取资源。下面,我们将详细介绍如何在 WinHttpClient
类中实现 GET 请求。
代码示例
bool WinHttpClient::Get(const std::wstring& url, std::wstring& response) {
// 解析 URL
URL_COMPONENTS urlComp = {0};
urlComp.dwStructSize = sizeof(urlComp);
wchar_t hostName[256];
wchar_t urlPath[1024];
urlComp.lpszHostName = hostName;
urlComp.dwHostNameLength = sizeof(hostName) / sizeof(wchar_t);
urlComp.lpszUrlPath = urlPath;
urlComp.dwUrlPathLength = sizeof(urlPath) / sizeof(wchar_t);
if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) {
// 错误处理
return false;
}
// 建立连接
hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (!hConnect) {
// 错误处理
return false;
}
// 创建请求
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", urlComp.lpszUrlPath,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (!hRequest) {
// 错误处理
return false;
}
// 发送请求
BOOL bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0,
0, 0);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 接收响应
bResults = WinHttpReceiveResponse(hRequest, NULL);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 读取响应数据
DWORD dwSize = 0;
do {
DWORD dwDownloaded = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
break;
if (dwSize == 0)
break;
wchar_t* buffer = new wchar_t[dwSize / sizeof(wchar_t) + 1];
ZeroMemory(buffer, dwSize + sizeof(wchar_t));
if (!WinHttpReadData(hRequest, (LPVOID)buffer, dwSize, &dwDownloaded)) {
delete[] buffer;
break;
}
response += buffer;
delete[] buffer;
} while (dwSize > 0);
// 清理句柄
WinHttpCloseHandle(hRequest);
return true;
}
详细解释
解析 URL:
URL_COMPONENTS urlComp = {0}; urlComp.dwStructSize = sizeof(urlComp); wchar_t hostName[256]; wchar_t urlPath[1024]; urlComp.lpszHostName = hostName; urlComp.dwHostNameLength = sizeof(hostName) / sizeof(wchar_t); urlComp.lpszUrlPath = urlPath; urlComp.dwUrlPathLength = sizeof(urlPath) / sizeof(wchar_t); if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) { // 错误处理 return false; }
- WinHttpCrackUrl:将完整的 URL 分解为各个组成部分,如主机名、路径、端口等。
- URL_COMPONENTS 结构:存储解析后的 URL 组件。
建立连接:
hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0); if (!hConnect) { // 错误处理 return false; }
- WinHttpConnect:建立与服务器的连接,返回连接句柄。
创建请求:
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", urlComp.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0); if (!hRequest) { // 错误处理 return false; }
- WinHttpOpenRequest:创建一个 HTTP 请求句柄,指定请求方法(GET)、路径、是否使用安全协议等。
发送请求:
BOOL bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0); if (!bResults) { // 错误处理 WinHttpCloseHandle(hRequest); return false; }
- WinHttpSendRequest:发送 HTTP 请求到服务器。对于 GET 请求,不需要额外的数据。
接收响应:
bResults = WinHttpReceiveResponse(hRequest, NULL); if (!bResults) { // 错误处理 WinHttpCloseHandle(hRequest); return false; }
- WinHttpReceiveResponse:等待并接收服务器的响应。
读取响应数据:
DWORD dwSize = 0; do { DWORD dwDownloaded = 0; if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break; if (dwSize == 0) break; wchar_t* buffer = new wchar_t[dwSize / sizeof(wchar_t) + 1]; ZeroMemory(buffer, dwSize + sizeof(wchar_t)); if (!WinHttpReadData(hRequest, (LPVOID)buffer, dwSize, &dwDownloaded)) { delete[] buffer; break; } response += buffer; delete[] buffer; } while (dwSize > 0);
- WinHttpQueryDataAvailable:查询可用的数据大小。
- WinHttpReadData:读取响应数据,并将其追加到
response
字符串中。
清理句柄:
WinHttpCloseHandle(hRequest); return true;
- WinHttpCloseHandle:关闭请求句柄,释放资源。
注意事项
- 内存管理:确保在读取数据后及时释放分配的内存,避免内存泄漏。
- 错误处理:每一步操作后应进行错误检查,确保程序的健壮性。
- 字符编码:本文示例使用宽字符(
wchar_t
),根据实际需求可调整为窄字符。
实现 POST 请求
POST 请求用于向服务器提交数据,如表单数据、文件上传等。与 GET 请求类似,POST 请求需要发送数据到服务器,并接收响应。下面,我们将在 WinHttpClient
类中实现 POST 请求。
代码示例
bool WinHttpClient::Post(const std::wstring& url, const std::wstring& data, std::wstring& response) {
// 解析 URL
URL_COMPONENTS urlComp = {0};
urlComp.dwStructSize = sizeof(urlComp);
wchar_t hostName[256];
wchar_t urlPath[1024];
urlComp.lpszHostName = hostName;
urlComp.dwHostNameLength = sizeof(hostName) / sizeof(wchar_t);
urlComp.lpszUrlPath = urlPath;
urlComp.dwUrlPathLength = sizeof(urlPath) / sizeof(wchar_t);
if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) {
// 错误处理
return false;
}
// 建立连接
hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (!hConnect) {
// 错误处理
return false;
}
// 创建请求
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", urlComp.lpszUrlPath,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (!hRequest) {
// 错误处理
return false;
}
// 设置请求头
const wchar_t* additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n";
if (!WinHttpAddRequestHeaders(hRequest, additionalHeaders, -1L, WINHTTP_ADDREQ_FLAG_ADD)) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 发送请求
BOOL bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
(LPVOID)data.c_str(), data.length() * sizeof(wchar_t),
data.length() * sizeof(wchar_t), 0);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 接收响应
bResults = WinHttpReceiveResponse(hRequest, NULL);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 读取响应数据
DWORD dwSize = 0;
do {
DWORD dwDownloaded = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
break;
if (dwSize == 0)
break;
wchar_t* buffer = new wchar_t[dwSize / sizeof(wchar_t) + 1];
ZeroMemory(buffer, dwSize + sizeof(wchar_t));
if (!WinHttpReadData(hRequest, (LPVOID)buffer, dwSize, &dwDownloaded)) {
delete[] buffer;
break;
}
response += buffer;
delete[] buffer;
} while (dwSize > 0);
// 清理句柄
WinHttpCloseHandle(hRequest);
return true;
}
详细解释
解析 URL:
与 GET 请求相同,使用 WinHttpCrackUrl 解析 URL。
建立连接:
使用 WinHttpConnect 建立与服务器的连接。
创建请求:
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", urlComp.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
- POST 方法指定为请求类型。
设置请求头:
const wchar_t* additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n"; if (!WinHttpAddRequestHeaders(hRequest, additionalHeaders, -1L, WINHTTP_ADDREQ_FLAG_ADD)) { // 错误处理 WinHttpCloseHandle(hRequest); return false; }
- Content-Type:指定请求体的类型,此处为表单数据。
- WinHttpAddRequestHeaders:添加额外的请求头信息。
发送请求:
BOOL bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)data.c_str(), data.length() * sizeof(wchar_t), data.length() * sizeof(wchar_t), 0); if (!bResults) { // 错误处理 WinHttpCloseHandle(hRequest); return false; }
- 请求数据:通过
data
参数传递要发送的数据。 - WinHttpSendRequest:发送带有数据的 POST 请求。
- 请求数据:通过
接收响应与读取数据:
与 GET 请求相同,使用 WinHttpReceiveResponse 接收响应,并通过 WinHttpReadData 读取响应内容。
清理句柄:
关闭请求句柄,释放资源。
注意事项
- 请求头设置:根据实际需求,可能需要设置更多的请求头,如
Authorization
、User-Agent
等。 - 数据编码:确保发送的数据符合服务器的预期编码格式,如
UTF-8
。 - 数据大小限制:注意 POST 请求的数据大小限制,避免发送过大的数据导致请求失败。
实现多线程文件下载
在网络编程中,多线程文件下载可以显著提高下载速度,特别是对于大文件。通过将文件分割成多个部分,并行下载各部分数据,最终合并成完整文件。下面,我们将在 WinHttpClient
类中实现多线程文件下载功能。
原理分析
- 获取文件大小:通过发送 HEAD 请求获取文件的总大小。
- 分割文件:根据线程数量,将文件分割成多个部分,每个线程负责下载一个部分。
- 并行下载:每个线程独立发送 GET 请求,下载对应的文件部分。
- 合并文件:所有线程下载完成后,将各部分数据按照顺序合并成完整文件。
代码示例
#include <thread>
#include <vector>
#include <fstream>
#include <mutex>
bool WinHttpClient::DownloadFile(const std::wstring& url, const std::wstring& savePath, int threadCount) {
// 获取文件大小
DWORD fileSize = 0;
// 发送 HEAD 请求获取 Content-Length
URL_COMPONENTS urlComp = {0};
urlComp.dwStructSize = sizeof(urlComp);
wchar_t hostName[256];
wchar_t urlPath[1024];
urlComp.lpszHostName = hostName;
urlComp.dwHostNameLength = sizeof(hostName) / sizeof(wchar_t);
urlComp.lpszUrlPath = urlPath;
urlComp.dwUrlPathLength = sizeof(urlPath) / sizeof(wchar_t);
if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) {
// 错误处理
return false;
}
// 建立连接
HINTERNET hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (!hConnect) {
// 错误处理
return false;
}
// 创建 HEAD 请求
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"HEAD", urlComp.lpszUrlPath,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (!hRequest) {
// 错误处理
return false;
}
// 发送请求
BOOL bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0,
0, 0);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 接收响应
bResults = WinHttpReceiveResponse(hRequest, NULL);
if (!bResults) {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 查询 Content-Length
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
wchar_t szBuffer[128];
if (WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&fileSize,
&dwSize,
WINHTTP_NO_HEADER_INDEX)) {
// 成功获取文件大小
} else {
// 错误处理
WinHttpCloseHandle(hRequest);
return false;
}
// 清理 HEAD 请求句柄
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
// 计算每个线程需要下载的字节范围
DWORD partSize = fileSize / threadCount;
std::vector<std::pair<DWORD, DWORD>> ranges;
for (int i = 0; i < threadCount; ++i) {
DWORD start = i * partSize;
DWORD end = (i == threadCount - 1) ? fileSize - 1 : (start + partSize - 1);
ranges.emplace_back(std::make_pair(start, end));
}
// 创建临时文件
std::ofstream ofs(savePath, std::ios::binary);
ofs.seekp(fileSize - 1);
ofs.write("", 1);
ofs.close();
// 多线程下载
std::vector<std::thread> threads;
std::mutex mtx;
bool success = true;
for (int i = 0; i < threadCount; ++i) {
threads.emplace_back([&, i]() {
// 建立连接
HINTERNET hConnectThread = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (!hConnectThread) {
std::lock_guard<std::mutex> lock(mtx);
success = false;
return;
}
// 创建 GET 请求
HINTERNET hRequestThread = WinHttpOpenRequest(hConnectThread, L"GET", urlComp.lpszUrlPath,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (!hRequestThread) {
WinHttpCloseHandle(hConnectThread);
std::lock_guard<std::mutex> lock(mtx);
success = false;
return;
}
// 设置 Range 头
wchar_t rangeHeader[64];
swprintf_s(rangeHeader, L"Range: bytes=%lu-%lu\r\n", ranges[i].first, ranges[i].second);
if (!WinHttpAddRequestHeaders(hRequestThread, rangeHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD)) {
WinHttpCloseHandle(hRequestThread);
WinHttpCloseHandle(hConnectThread);
std::lock_guard<std::mutex> lock(mtx);
success = false;
return;
}
// 发送请求
BOOL bResultsThread = WinHttpSendRequest(hRequestThread,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0,
0, 0);
if (!bResultsThread) {
WinHttpCloseHandle(hRequestThread);
WinHttpCloseHandle(hConnectThread);
std::lock_guard<std::mutex> lock(mtx);
success = false;
return;
}
// 接收响应
bResultsThread = WinHttpReceiveResponse(hRequestThread, NULL);
if (!bResultsThread) {
WinHttpCloseHandle(hRequestThread);
WinHttpCloseHandle(hConnectThread);
std::lock_guard<std::mutex> lock(mtx);
success = false;
return;
}
// 读取数据
DWORD dwSizeThread = 0;
do {
DWORD dwDownloadedThread = 0;
if (!WinHttpQueryDataAvailable(hRequestThread, &dwSizeThread))
break;
if (dwSizeThread == 0)
break;
char* buffer = new char[dwSizeThread];
ZeroMemory(buffer, dwSizeThread);
if (!WinHttpReadData(hRequestThread, (LPVOID)buffer, dwSizeThread, &dwDownloadedThread)) {
delete[] buffer;
break;
}
// 写入文件
std::ofstream ofsThread(savePath, std::ios::binary | std::ios::in | std::ios::out);
ofsThread.seekp(ranges[i].first);
ofsThread.write(buffer, dwDownloadedThread);
ofsThread.close();
delete[] buffer;
} while (dwSizeThread > 0);
// 清理句柄
WinHttpCloseHandle(hRequestThread);
WinHttpCloseHandle(hConnectThread);
});
}
// 等待所有线程完成
for (auto& th : threads) {
if (th.joinable())
th.join();
}
return success;
}
详细解释
获取文件大小:
- HEAD 请求:发送 HEAD 请求获取服务器响应头中的
Content-Length
,从而确定文件的总大小。
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"HEAD", urlComp.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
- WinHttpQueryHeaders:查询
Content-Length
以获取文件大小。
- HEAD 请求:发送 HEAD 请求获取服务器响应头中的
计算下载范围:
根据文件大小和线程数量,计算每个线程负责下载的字节范围。
DWORD partSize = fileSize / threadCount; std::vector<std::pair<DWORD, DWORD>> ranges; for (int i = 0; i < threadCount; ++i) { DWORD start = i * partSize; DWORD end = (i == threadCount - 1) ? fileSize - 1 : (start + partSize - 1); ranges.emplace_back(std::make_pair(start, end)); }
创建临时文件:
创建一个指定大小的空文件,预留下载空间。
std::ofstream ofs(savePath, std::ios::binary); ofs.seekp(fileSize - 1); ofs.write("", 1); ofs.close();
多线程下载:
利用 C++11 的
std::thread
,为每个下载部分创建一个线程,独立执行下载任务。std::vector<std::thread> threads; std::mutex mtx; bool success = true; for (int i = 0; i < threadCount; ++i) { threads.emplace_back([&, i]() { // 下载逻辑 }); } // 等待所有线程完成 for (auto& th : threads) { if (th.joinable()) th.join(); }
- Lambda 表达式:定义每个线程的下载任务。
- 互斥锁:确保在多线程环境下的安全操作,如设置下载成功标志。
线程内下载逻辑:
每个线程独立发送 GET 请求,设置
Range
头,下载指定范围的数据,并写入到临时文件的对应位置。// 设置 Range 头 wchar_t rangeHeader[64]; swprintf_s(rangeHeader, L"Range: bytes=%lu-%lu\r\n", ranges[i].first, ranges[i].second); if (!WinHttpAddRequestHeaders(hRequestThread, rangeHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD)) { // 错误处理 } // 读取数据并写入文件 std::ofstream ofsThread(savePath, std::ios::binary | std::ios::in | std::ios::out); ofsThread.seekp(ranges[i].first); ofsThread.write(buffer, dwDownloadedThread); ofsThread.close();
合并文件:
由于所有线程直接写入到临时文件的对应位置,最终无需额外的合并步骤,文件即为完整下载。
注意事项
- 线程同步:确保多个线程同时写入文件时不会发生数据冲突,本文通过每个线程写入不同的文件部分来避免冲突。
- 错误处理:任意一个线程下载失败,整个下载过程应视为失败,必要时可实现重试机制。
- 性能优化:根据网络带宽和服务器支持情况,合理设置线程数量,避免过多线程导致资源浪费。
错误处理
在网络编程中,错误处理至关重要。正确的错误处理不仅可以提升应用的稳定性,还能帮助开发者快速定位问题。以下是 WinHttpClient
类中常见的错误处理策略。
常见错误类型
- 网络连接错误:如服务器不可达、DNS 解析失败等。
- 请求发送失败:如请求头格式错误、请求超时等。
- 响应接收失败:如服务器返回错误代码、响应数据损坏等。
- 数据读取错误:如读取数据失败、写入文件失败等。
错误处理策略
详细错误信息:
每次 WinHttp 操作失败后,调用 WinHttpGetLastError 获取详细的错误代码,并转换为可读的错误信息。
DWORD dwError = GetLastError(); // 根据 dwError 进行相应的错误处理
日志记录:
将错误信息记录到日志文件或控制台,便于后续分析和调试。
std::wcerr << L"WinHttp 错误:" << dwError << std::endl;
资源清理:
在发生错误后,确保及时关闭所有 WinHttp 句柄,释放资源,避免资源泄漏。
WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect);
异常处理:
使用 C++ 的异常机制,抛出自定义异常,统一处理错误。
if (!bResults) { throw std::runtime_error("发送请求失败"); }
重试机制:
对于可恢复的错误,如网络暂时中断,可以实现自动重试机制,提升应用的鲁棒性。
int retryCount = 3; while (retryCount > 0) { // 尝试操作 if (操作成功) break; retryCount--; } if (retryCount == 0) { // 记录错误并退出 }
示例:错误处理实现
bool WinHttpClient::Get(const std::wstring& url, std::wstring& response) {
// ... [前置代码省略]
if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) {
DWORD dwError = GetLastError();
std::wcerr << L"WinHttpCrackUrl 错误:" << dwError << std::endl;
return false;
}
// ... [后续代码省略]
if (!bResults) {
DWORD dwError = GetLastError();
std::wcerr << L"WinHttpSendRequest 错误:" << dwError << std::endl;
WinHttpCloseHandle(hRequest);
return false;
}
// ... [继续处理]
}
解释:
- 在每次 WinHttp 操作失败后,获取并输出错误代码,帮助开发者快速定位问题。
使用示例
为了更好地理解 WinHttpClient
类的使用方式,以下提供一个完整的示例,演示如何进行 GET 请求、POST 请求以及多线程文件下载。
示例代码
#include "WinHttpClient.h"
int main() {
WinHttpClient client;
// 示例 1:GET 请求
std::wstring getUrl = L"https://api.example.com/data";
std::wstring getResponse;
if (client.Get(getUrl, getResponse)) {
std::wcout << L"GET 响应:" << getResponse << std::endl;
} else {
std::wcout << L"GET 请求失败" << std::endl;
}
// 示例 2:POST 请求
std::wstring postUrl = L"https://api.example.com/submit";
std::wstring postData = L"name=John&age=30";
std::wstring postResponse;
if (client.Post(postUrl, postData, postResponse)) {
std::wcout << L"POST 响应:" << postResponse << std::endl;
} else {
std::wcout << L"POST 请求失败" << std::endl;
}
// 示例 3:多线程文件下载
std::wstring fileUrl = L"https://example.com/largefile.zip";
std::wstring savePath = L"C:\\Downloads\\largefile.zip";
if (client.DownloadFile(fileUrl, savePath, 4)) {
std::wcout << L"文件下载成功,保存路径:" << savePath << std::endl;
} else {
std::wcout << L"文件下载失败" << std::endl;
}
return 0;
}
详细解释
创建 WinHttpClient 实例:
WinHttpClient client;
- 初始化 WinHttp 会话,准备发送请求。
发送 GET 请求:
std::wstring getUrl = L"https://api.example.com/data"; std::wstring getResponse; if (client.Get(getUrl, getResponse)) { std::wcout << L"GET 响应:" << getResponse << std::endl; } else { std::wcout << L"GET 请求失败" << std::endl; }
- 发送 GET 请求至指定 URL,接收响应内容并输出。
发送 POST 请求:
std::wstring postUrl = L"https://api.example.com/submit"; std::wstring postData = L"name=John&age=30"; std::wstring postResponse; if (client.Post(postUrl, postData, postResponse)) { std::wcout << L"POST 响应:" << postResponse << std::endl; } else { std::wcout << L"POST 请求失败" << std::endl; }
- 发送 POST 请求至指定 URL,提交表单数据,接收并输出响应内容。
多线程文件下载:
std::wstring fileUrl = L"https://example.com/largefile.zip"; std::wstring savePath = L"C:\\Downloads\\largefile.zip"; if (client.DownloadFile(fileUrl, savePath, 4)) { std::wcout << L"文件下载成功,保存路径:" << savePath << std::endl; } else { std::wcout << L"文件下载失败" << std::endl; }
- 使用 4 个线程并行下载指定 URL 的大文件,保存至本地路径。
运行结果
GET 响应:{"status":"success","data":{...}}
POST 响应:{"status":"submitted","id":12345}
文件下载成功,保存路径:C:\Downloads\largefile.zip
解释:
- GET 响应:成功获取服务器返回的 JSON 数据。
- POST 响应:成功提交表单数据,并获取服务器返回的提交结果。
- 文件下载:成功下载大文件,并保存至指定路径。
分析说明表 📊
以下表格总结了 WinHttpClient
类中各功能模块的关键点及其实现细节。
功能模块 | 关键点 | 实现细节 |
---|---|---|
GET 请求 | 解析 URL、建立连接、发送请求、接收响应、读取数据 | 使用 WinHttpCrackUrl 解析 URL,WinHttpConnect 建立连接,WinHttpOpenRequest 创建请求,WinHttpSendRequest 发送请求,WinHttpReceiveResponse 接收响应,WinHttpReadData 读取数据 |
POST 请求 | 解析 URL、建立连接、设置请求头、发送数据、接收响应、读取数据 | 在 GET 请求基础上,使用 WinHttpAddRequestHeaders 设置 Content-Type ,并在 WinHttpSendRequest 中发送数据 |
多线程下载 | 获取文件大小、计算下载范围、创建临时文件、并行下载、合并文件 | 通过 HEAD 请求获取 Content-Length ,使用 C++11 线程分割下载任务,每个线程设置 Range 头并写入临时文件 |
错误处理 | 获取错误代码、记录日志、资源清理、异常处理、重试机制 | 每次 WinHttp 操作失败后,调用 GetLastError 获取错误代码,记录日志,关闭句柄,抛出异常或重试操作 |
资源管理 | 初始化会话、清理句柄、管理内存 | 在构造函数中初始化会话,析构函数中关闭所有句柄,确保内存分配后及时释放 |
灵活配置 | 自定义请求头、超时时间、代理设置 | 通过方法参数或配置文件设置额外请求头、超时时间,支持代理服务器配置 |
工作流程图 🛠️
以下是 WinHttpClient
类中 GET 请求和多线程文件下载的工作流程图,采用 Mermaid 语法绘制。
GET 请求工作流程
graph TD;
A[开始 GET 请求] --> B[解析 URL];
B --> C[建立连接];
C --> D[创建 GET 请求];
D --> E[发送请求];
E --> F[接收响应];
F --> G[读取响应数据];
G --> H[完成 GET 请求];
多线程文件下载工作流程
graph TD;
A[开始下载] --> B[发送 HEAD 请求获取文件大小];
B --> C[计算下载范围];
C --> D[创建临时文件];
D --> E[启动多个下载线程];
E --> F[每个线程发送 GET 请求设置 Range 头];
F --> G[每个线程下载数据并写入文件];
G --> H[所有线程完成];
H --> I[完成文件下载];
解释:
- GET 请求流程:从开始到完成,每一步依次进行,确保请求和响应的顺利完成。
- 多线程下载流程:从获取文件大小到启动多个线程并行下载,最终完成文件的下载和保存。
总结 🎯
本文详细介绍了如何封装一个 WinHttp 类,以支持 GET、POST 请求以及 多线程文件下载。通过以下几个关键步骤,您可以高效地进行网络编程:
- 封装 WinHttp 会话:通过构造函数和析构函数管理 WinHttp 会话的初始化和清理。
- 实现 GET 和 POST 请求:提供简洁的方法接口,支持常见的 HTTP 请求操作。
- 多线程文件下载:利用多线程技术,提高大文件下载的效率和速度。
- 错误处理与资源管理:确保每一步操作的错误都能被及时捕捉和处理,避免资源泄漏和程序崩溃。
- 灵活配置与扩展:支持自定义请求头、超时时间等,满足不同场景的需求。
通过系统化的设计和实现,WinHttpClient
类不仅提升了代码的可读性和可维护性,还显著增强了应用的性能和用户体验。无论是在客户端应用还是服务器端服务中,合理利用 WinHttp,都能为您的项目带来可靠的网络通信能力。
参考命令详解 🔍
WinHttpCrackUrl
功能: 解析 URL,分解为各个组成部分,如协议、主机名、路径、端口等。
参数解释:
url.c_str()
:要解析的完整 URL。0
:标志位,通常设为 0。0
:标志位,通常设为 0。&urlComp
:指向URL_COMPONENTS
结构的指针,存储解析后的结果。
示例:
if (!WinHttpCrackUrl(url.c_str(), 0, 0, &urlComp)) {
// 错误处理
}
WinHttpConnect
功能: 建立与服务器的连接,返回连接句柄。
参数解释:
hSession
:已初始化的 WinHttp 会话句柄。urlComp.lpszHostName
:主机名。urlComp.nPort
:端口号。0
:保留参数,通常设为 0。
示例:
hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
if (!hConnect) {
// 错误处理
}
WinHttpOpenRequest
功能: 创建一个 HTTP 请求句柄,指定请求方法、路径、协议等。
参数解释:
hConnect
:与服务器的连接句柄。L"GET"
或L"POST"
:请求方法。urlComp.lpszUrlPath
:请求路径。NULL
:默认协议。WINHTTP_NO_REFERER
:无引用页。WINHTTP_DEFAULT_ACCEPT_TYPES
:默认接受类型。(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0
:是否使用安全协议。
示例:
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", urlComp.lpszUrlPath,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
(urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0);
if (!hRequest) {
// 错误处理
}
WinHttpAddRequestHeaders
功能: 添加额外的请求头信息。
参数解释:
hRequest
:请求句柄。additionalHeaders
:要添加的请求头字符串。-1L
:请求头字符串的长度,-1 表示自动计算。WINHTTP_ADDREQ_FLAG_ADD
:添加请求头而非替换。
示例:
const wchar_t* additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n";
if (!WinHttpAddRequestHeaders(hRequest, additionalHeaders, -1L, WINHTTP_ADDREQ_FLAG_ADD)) {
// 错误处理
}
WinHttpSendRequest
功能: 发送 HTTP 请求到服务器。
参数解释:
hRequest
:请求句柄。WINHTTP_NO_ADDITIONAL_HEADERS
:无额外请求头。0
:请求头长度。WINHTTP_NO_REQUEST_DATA
或data.c_str()
:请求体数据。0
或data.length() * sizeof(wchar_t)
:请求体数据长度。0
:总数据长度。0
:上下文标识。
示例:
BOOL bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
(LPVOID)data.c_str(), data.length() * sizeof(wchar_t),
data.length() * sizeof(wchar_t), 0);
if (!bResults) {
// 错误处理
}
WinHttpReceiveResponse
功能: 等待并接收服务器的响应。
参数解释:
hRequest
:请求句柄。NULL
:保留参数,通常设为 NULL。
示例:
bResults = WinHttpReceiveResponse(hRequest, NULL);
if (!bResults) {
// 错误处理
}
WinHttpQueryHeaders
功能: 查询响应头中的特定字段,如 Content-Length
。
参数解释:
hRequest
:请求句柄。WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER
:查询Content-Length
,并以数字形式返回。WINHTTP_HEADER_NAME_BY_INDEX
:按索引查询。&fileSize
:存储查询结果的变量。&dwSize
:存储数据大小的变量。WINHTTP_NO_HEADER_INDEX
:无特定头索引。
示例:
if (WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&fileSize,
&dwSize,
WINHTTP_NO_HEADER_INDEX)) {
// 成功获取文件大小
} else {
// 错误处理
}
WinHttpQueryDataAvailable
功能: 查询可用的数据大小。
参数解释:
hRequest
:请求句柄。&dwSize
:存储可用数据大小的变量。
示例:
if (!WinHttpQueryDataAvailable(hRequest, &dwSize))
break;
WinHttpReadData
功能: 读取响应数据。
参数解释:
hRequest
:请求句柄。(LPVOID)buffer
:数据缓冲区。dwSize
:要读取的数据大小。&dwDownloaded
:实际读取的数据大小。
示例:
if (!WinHttpReadData(hRequest, (LPVOID)buffer, dwSize, &dwDownloaded)) {
// 错误处理
}
WinHttpCloseHandle
功能: 关闭 WinHttp 句柄,释放资源。
参数解释:
hRequest
或hConnect
:要关闭的句柄。
示例:
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpAddRequestHeaders
在多线程下载中的使用
功能: 设置 Range
头,以指定下载的字节范围。
示例:
wchar_t rangeHeader[64];
swprintf_s(rangeHeader, L"Range: bytes=%lu-%lu\r\n", ranges[i].first, ranges[i].second);
if (!WinHttpAddRequestHeaders(hRequestThread, rangeHeader, -1L, WINHTTP_ADDREQ_FLAG_ADD)) {
// 错误处理
}
解释:
- Range 头:指定要下载的文件部分范围,实现分段下载。
std::thread
的使用
功能: 创建和管理线程,实现并行下载。
示例:
std::thread th([&, i]() {
// 线程下载任务
});
解释:
- Lambda 表达式:定义线程内的下载逻辑。
- 捕获列表
[&, i]
:捕获所有外部变量引用和线程索引i
。
重要提示 🛑
- 数据备份:在进行任何涉及文件写入或网络操作的操作前,务必备份重要数据,避免数据丢失。
- 权限管理:确保应用程序具备必要的网络访问权限和文件写入权限,避免因权限不足导致操作失败。
- 错误处理:在实际应用中,增强错误处理机制,确保在各种异常情况下应用能稳定运行。
- 资源管理:合理管理 WinHttp 句柄和线程资源,避免资源泄漏和竞争条件。
- 线程数量优化:根据实际网络带宽和服务器负载,合理设置多线程下载的线程数量,避免过多线程导致的资源浪费或服务器压力。
- 安全通信:在涉及敏感数据传输时,确保使用 HTTPS 协议,保障数据传输的安全性。
- 性能测试:在大规模应用前,进行充分的性能测试,确保应用在高负载下依然稳定高效。
通过遵循上述提示和最佳实践,您可以确保 WinHttpClient
类在各种场景下都能稳定、高效地运行,满足应用的网络通信需求。