Quantcast
Channel: 小蓝博客
Viewing all articles
Browse latest Browse all 3155

WinHttp类封装:支持GET、POST、多线程文件下载

$
0
0

WinHttp 类封装:支持 GET、POST、多线程文件下载 🖥️📂

在现代应用开发中,网络请求是不可或缺的一部分。WinHttp 是 Windows 平台上提供的一套强大的 HTTP 服务接口,广泛用于实现客户端与服务器之间的通信。为了简化和优化网络请求操作,本文将详细介绍如何封装一个支持 GETPOST 请求以及 多线程文件下载 的 WinHttp 类。通过本教程,您将能够高效地进行网络编程,提高应用的性能和响应速度。

目录

  1. WinHttp 简介
  2. WinHttp 封装类设计
  3. 实现 GET 请求
  4. 实现 POST 请求
  5. 实现多线程文件下载
  6. 错误处理
  7. 使用示例
  8. 分析说明表 📊
  9. 工作流程图 🛠️
  10. 总结 🎯

WinHttp 简介

WinHttp(Windows HTTP Services)是微软为 Windows 平台提供的一套 HTTP 协议客户端 API。它支持多种 HTTP 操作,如发送请求、接收响应、处理重定向、管理连接等。相比于传统的 WinInet,WinHttp 更加轻量且适用于服务器端应用程序,具备更高的性能和更好的资源管理能力。

WinHttp 的主要功能包括:

  • HTTP/HTTPS 请求:支持各种 HTTP 方法,如 GET、POST、PUT、DELETE 等。
  • 代理支持:支持通过代理服务器进行请求。
  • 异步操作:支持异步模式,提升应用的响应性。
  • 多线程支持:适用于多线程环境,提升并发性能。
  • 证书管理:支持 SSL/TLS 协议,提供安全通信。

WinHttp 封装类设计

为了简化 WinHttp 的使用,提升代码的可读性和可维护性,我们将封装一个 WinHttpClient 类。该类将提供易于调用的方法来执行 GETPOST 请求以及支持 多线程文件下载

类设计要点

  1. 封装初始化与清理:在构造函数中初始化 WinHttp 会话,在析构函数中进行资源清理。
  2. 支持 GET 和 POST 请求:提供简洁的方法接口,允许用户轻松发送 GET 和 POST 请求。
  3. 多线程文件下载:利用多线程技术,支持将大文件分割成多个部分并行下载,提高下载速度。
  4. 错误处理:提供详细的错误信息,便于调试和维护。
  5. 灵活配置:允许用户自定义请求头、超时时间等参数。

类结构示意

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;
}

详细解释

  1. 解析 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 组件。
  2. 建立连接

    hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, urlComp.nPort, 0);
    if (!hConnect) {
        // 错误处理
        return false;
    }
    • WinHttpConnect:建立与服务器的连接,返回连接句柄。
  3. 创建请求

    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)、路径、是否使用安全协议等。
  4. 发送请求

    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 请求,不需要额外的数据。
  5. 接收响应

    bResults = WinHttpReceiveResponse(hRequest, NULL);
    if (!bResults) {
        // 错误处理
        WinHttpCloseHandle(hRequest);
        return false;
    }
    • WinHttpReceiveResponse:等待并接收服务器的响应。
  6. 读取响应数据

    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 字符串中。
  7. 清理句柄

    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;
}

详细解释

  1. 解析 URL

    与 GET 请求相同,使用 WinHttpCrackUrl 解析 URL。

  2. 建立连接

    使用 WinHttpConnect 建立与服务器的连接。

  3. 创建请求

    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 方法指定为请求类型。
  4. 设置请求头

    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:添加额外的请求头信息。
  5. 发送请求

    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 请求。
  6. 接收响应与读取数据

    与 GET 请求相同,使用 WinHttpReceiveResponse 接收响应,并通过 WinHttpReadData 读取响应内容。

  7. 清理句柄

    关闭请求句柄,释放资源。

注意事项

  • 请求头设置:根据实际需求,可能需要设置更多的请求头,如 AuthorizationUser-Agent 等。
  • 数据编码:确保发送的数据符合服务器的预期编码格式,如 UTF-8
  • 数据大小限制:注意 POST 请求的数据大小限制,避免发送过大的数据导致请求失败。

实现多线程文件下载

在网络编程中,多线程文件下载可以显著提高下载速度,特别是对于大文件。通过将文件分割成多个部分,并行下载各部分数据,最终合并成完整文件。下面,我们将在 WinHttpClient 类中实现多线程文件下载功能。

原理分析

  1. 获取文件大小:通过发送 HEAD 请求获取文件的总大小。
  2. 分割文件:根据线程数量,将文件分割成多个部分,每个线程负责下载一个部分。
  3. 并行下载:每个线程独立发送 GET 请求,下载对应的文件部分。
  4. 合并文件:所有线程下载完成后,将各部分数据按照顺序合并成完整文件。

代码示例

#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;
}

详细解释

  1. 获取文件大小

    • 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 以获取文件大小。
  2. 计算下载范围

    根据文件大小和线程数量,计算每个线程负责下载的字节范围。

    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));
    }
  3. 创建临时文件

    创建一个指定大小的空文件,预留下载空间。

    std::ofstream ofs(savePath, std::ios::binary);
    ofs.seekp(fileSize - 1);
    ofs.write("", 1);
    ofs.close();
  4. 多线程下载

    利用 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 表达式:定义每个线程的下载任务。
    • 互斥锁:确保在多线程环境下的安全操作,如设置下载成功标志。
  5. 线程内下载逻辑

    每个线程独立发送 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();
  6. 合并文件

    由于所有线程直接写入到临时文件的对应位置,最终无需额外的合并步骤,文件即为完整下载。

注意事项

  • 线程同步:确保多个线程同时写入文件时不会发生数据冲突,本文通过每个线程写入不同的文件部分来避免冲突。
  • 错误处理:任意一个线程下载失败,整个下载过程应视为失败,必要时可实现重试机制。
  • 性能优化:根据网络带宽和服务器支持情况,合理设置线程数量,避免过多线程导致资源浪费。

错误处理

在网络编程中,错误处理至关重要。正确的错误处理不仅可以提升应用的稳定性,还能帮助开发者快速定位问题。以下是 WinHttpClient 类中常见的错误处理策略。

常见错误类型

  1. 网络连接错误:如服务器不可达、DNS 解析失败等。
  2. 请求发送失败:如请求头格式错误、请求超时等。
  3. 响应接收失败:如服务器返回错误代码、响应数据损坏等。
  4. 数据读取错误:如读取数据失败、写入文件失败等。

错误处理策略

  1. 详细错误信息

    每次 WinHttp 操作失败后,调用 WinHttpGetLastError 获取详细的错误代码,并转换为可读的错误信息。

    DWORD dwError = GetLastError();
    // 根据 dwError 进行相应的错误处理
  2. 日志记录

    将错误信息记录到日志文件或控制台,便于后续分析和调试。

    std::wcerr << L"WinHttp 错误:" << dwError << std::endl;
  3. 资源清理

    在发生错误后,确保及时关闭所有 WinHttp 句柄,释放资源,避免资源泄漏。

    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
  4. 异常处理

    使用 C++ 的异常机制,抛出自定义异常,统一处理错误。

    if (!bResults) {
        throw std::runtime_error("发送请求失败");
    }
  5. 重试机制

    对于可恢复的错误,如网络暂时中断,可以实现自动重试机制,提升应用的鲁棒性。

    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;
}

详细解释

  1. 创建 WinHttpClient 实例

    WinHttpClient client;
    • 初始化 WinHttp 会话,准备发送请求。
  2. 发送 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,接收响应内容并输出。
  3. 发送 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,提交表单数据,接收并输出响应内容。
  4. 多线程文件下载

    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 类,以支持 GETPOST 请求以及 多线程文件下载。通过以下几个关键步骤,您可以高效地进行网络编程:

  1. 封装 WinHttp 会话:通过构造函数和析构函数管理 WinHttp 会话的初始化和清理。
  2. 实现 GET 和 POST 请求:提供简洁的方法接口,支持常见的 HTTP 请求操作。
  3. 多线程文件下载:利用多线程技术,提高大文件下载的效率和速度。
  4. 错误处理与资源管理:确保每一步操作的错误都能被及时捕捉和处理,避免资源泄漏和程序崩溃。
  5. 灵活配置与扩展:支持自定义请求头、超时时间等,满足不同场景的需求。

通过系统化的设计和实现,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_DATAdata.c_str():请求体数据。
  • 0data.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 句柄,释放资源。

参数解释:

  • hRequesthConnect:要关闭的句柄。

示例:

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

重要提示 🛑

  1. 数据备份:在进行任何涉及文件写入或网络操作的操作前,务必备份重要数据,避免数据丢失。
  2. 权限管理:确保应用程序具备必要的网络访问权限和文件写入权限,避免因权限不足导致操作失败。
  3. 错误处理:在实际应用中,增强错误处理机制,确保在各种异常情况下应用能稳定运行。
  4. 资源管理:合理管理 WinHttp 句柄和线程资源,避免资源泄漏和竞争条件。
  5. 线程数量优化:根据实际网络带宽和服务器负载,合理设置多线程下载的线程数量,避免过多线程导致的资源浪费或服务器压力。
  6. 安全通信:在涉及敏感数据传输时,确保使用 HTTPS 协议,保障数据传输的安全性。
  7. 性能测试:在大规模应用前,进行充分的性能测试,确保应用在高负载下依然稳定高效。

通过遵循上述提示和最佳实践,您可以确保 WinHttpClient 类在各种场景下都能稳定、高效地运行,满足应用的网络通信需求。


Viewing all articles
Browse latest Browse all 3155

Latest Images

Trending Articles