一、类设计概述
近期准备用C++做一些大预言模型方面的开发,先期计划实现C++调用公共的大模型Web接口,因为之前没做过C++的Web开发,经验少,所以对比了一些主流的框架,包括实际测试验证。以下是Windows平台下主流C++ HTTP库的对比分析:
库名称 | 内置支持 | 第三方依赖 | HTTPS支持 | API复杂度 | 跨平台 | 社区活跃度 | 典型应用场景 |
---|---|---|---|---|---|---|---|
WinHTTP | ✅ 是(Windows SDK) | 无(自动处理SSL/TLS) | ✅ 原生支持(无需OpenSSL) | 中等 | ❌ 仅Windows | 中等 | 系统级应用、快速集成、Windows专属开发 |
libcurl | ❌ 需手动集成 | 需OpenSSL/zlib等 | ✅ 依赖第三方库配置 | 高 | ✅ 是 | 高 | 跨平台项目、复杂协议需求 |
libhv | ❌ 需手动集成 | 无(内置SSL支持) | ✅ 需编译时启用 | 中等 | ✅ 是 | 中等 | 高性能网络服务、异步IO场景 |
Boost.Beast | ❌ 需Boost库 | 需OpenSSL | ✅ 需手动配置 | 高 | ✅ 是 | 高 | 现代C++项目、需要高度定制化 |
cpp-netlib | ❌ 需手动集成 | 需Boost/Asio等 | ✅ 需依赖项支持 | 高 | ✅ 是 | 低 | 传统企业级应用、遗留系统维护 |
Poco | ❌ 需手动集成 | 需OpenSSL | ✅ 需手动配置 | 中等 | ✅ 是 | 中等 | 综合性框架需求、企业级开发 |
Crow | ❌ 需手动集成 | 无(仅HTTP) | ❌ 无原生支持 | 低 | ✅ 是 | 低 | 轻量级Web服务、快速原型开发 |
转了一圈,最终选择了Windows原生WinHTTP API实现。
选型原因分析
-
内置支持与零配置优势
- WinHTTP直接集成于Windows系统,无需额外安装或配置OpenSSL等依赖,避免了证书链、动态库路径等复杂问题。对于Windows专属应用,可直接调用
winhttp.lib
,实现"开箱即用",也支持流式接口。
- WinHTTP直接集成于Windows系统,无需额外安装或配置OpenSSL等依赖,避免了证书链、动态库路径等复杂问题。对于Windows专属应用,可直接调用
-
简化HTTPS开发
- WinHTTP自动处理TLS/SSL握手和证书验证(通过
SECURITY_FLAG_IGNORE_*
标志控制),开发者无需手动管理加密套件或协议版本,显著降低HTTPS开发门槛。
- WinHTTP自动处理TLS/SSL握手和证书验证(通过
-
资源占用与性能平衡
- 相比libhv或Boost.Beast等跨平台库,WinHTTP在Windows环境下表现出更优的资源利用率,尤其适合对内存占用敏感的系统工具或后台服务。
-
API复杂度可控
- 虽然WinHTTP API较为底层,但通过封装(如您提供的
SimpleHttp
类)可屏蔽复杂性,同时保留对请求头、超时等关键参数的控制能力。
- 虽然WinHTTP API较为底层,但通过封装(如您提供的
-
企业级安全合规
- WinHTTP通过微软签名验证,符合Windows安全合规要求,适合金融、政务等对供应链安全敏感的场景。
SimpleHttp核心特性
SimpleHttp 是基于 Windows WinHTTP API 封装的 C++ HTTP 客户端类,支持同步/异步 HTTP/HTTPS 请求,提供流式数据处理能力。特性包括:
- 自适应HTTP/HTTPS协议
- 支持流式传输(Streaming)
- 线程安全的数据队列
- 异常安全的资源管理
- 符合C++20标准的现代语法
类接口说明
公共接口说明
1. 构造函数
SimpleHttp(const std::string& home, bool https = false);
- 功能:初始化 WinHTTP 会话,设置基地址和协议类型
- 参数:
home
:服务器基地址(如"api.example.com"
)https
:是否使用 HTTPS(默认false
)
- 异常:会话初始化失败时抛出
std::runtime_error
2. 析构函数
~SimpleHttp();
- 功能:释放所有资源,确保流式线程安全退出
- 行为:
- 终止流式接收线程(若有)
- 关闭所有 WinHTTP 句柄
3. GET 请求
int Get(const std::string& upath, std::string& resp, bool stream = false);
- 功能:发起 GET 请求
- 参数:
upath
:接口路径(自动拼接基地址)resp
:同步模式下存储完整响应stream
:是否启用流式处理(默认false
)
- 返回值:
0
:成功(流式请求立即返回)-1
:失败(异常抛出前返回)
- 异常:网络错误或 HTTP 状态码非 200 时抛出
std::runtime_error
4. POST 请求
int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false);
- 功能:发起 POST 请求
- 参数:
upath
:接口路径para
:JSON 格式请求体resp
:同步模式下存储完整响应stream
:是否启用流式处理
- 返回值:同
Get()
方法 - 默认头:自动添加
Content-Type: application/json
(可覆盖)
5. 流式响应获取
int TryFetchResp(std::string& resp);
- 功能:轮询获取流式数据块
- 参数:
resp
存储当前数据块 - 返回值:
>0
:数据长度0
:流结束-1
:暂无数据
- 线程安全:通过互斥锁保护队列
6. 设置请求头
void SetHeaders(const std::unordered_map<std::string, std::string>& headers);
- 功能:批量设置请求头
- 参数:键值对映射(如
{ {"Authorization", "Bearer token"} }
) - 合并策略:相同头字段自动合并(
WINHTTP_ADDREQ_FLAG_COALESCE
)
异常处理
- 抛出类型:
std::runtime_error
- 典型场景:
- 网络连接失败(如 DNS 解析错误)
- 请求创建失败
- HTTP 状态码非 200
- 流式线程异常终止
流式处理工作流程
- 调用
Get()
/Post()
时设置stream = true
- 轮询调用
TryFetchResp()
获取数据 - 当返回
0
时终止循环
示例代码:
SimpleHttp client("https://api.example.com", true);
client.Get("/stream", response, true);std::string chunk;
do {int size = client.TryFetchResp(chunk);if (size > 0) Process(chunk);else std::this_thread::sleep_for(std::chrono::milliseconds(100));
} while (size != 0);
实现细节
- 线程管理:流式请求启动独立线程持续接收数据
- 资源释放:析构函数确保线程和句柄正确关闭
- 编码转换:内部使用 UTF-8 处理字符串(
wstring2s
/string2w
)
限制与注意事项
- 仅支持 Windows 平台(依赖 WinHTTP)
- 流式请求需手动管理生命周期
- HTTPS 默认忽略证书错误(生产环境需修改安全标志)
版本兼容性
- Windows SDK:最低支持 Windows 7/Server 2008 R2
- 编译器:需支持 C++17
二、核心函数深度分析
封装的类比较简单,支持GET和POST请求,支持流式接口,下面对封装的每个部分逐一展开说明。
1. 构造函数
SimpleHttp(const std::string& home, bool https = false)
实现要点:
- 会话初始化:通过
WinHttpOpen
创建持久化会话,设置用户代理为"SimpleHttp/1.0" - 超时控制:统一设置5秒超时(连接/发送/接收/空闲)
- 异常处理:会话创建失败时抛出
runtime_error
,包含系统错误码 - 协议识别:通过
_isHttps
标记自动区分协议类型
设计考量:
- 使用默认代理配置(
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY
)提高兼容性 - 零初始化参数(
WINHTTP_NO_PROXY_NAME
)避免冗余配置
2. 析构函数
~SimpleHttp()
资源管理:
- 线程安全终止:通过原子变量
_streamActive
控制流式线程退出 - 句柄清理:
- 顺序关闭请求→连接→会话句柄
- 使用RAII模式确保资源释放
异常防御:
joinable()
检查避免二次join导致崩溃- 线程终止前设置
_streamActive=false
防止死锁
3. Get请求
int Get(const std::string& upath, std::string& resp, bool stream = false)
工作流程:
- 调用
SendRequest
发起GET请求 - 根据stream参数选择同步/异步模式
- 同步模式直接返回完整响应
- 异步模式启动子线程持续接收数据
参数设计:
upath
:URL路径自动拼接基地址stream
:控制响应处理模式(内存缓冲/流式队列)
4. Post请求
int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false)
增强特性:
- 自动设置Content-Type为application/json(未指定时)
- 支持任意格式POST数据(通过para参数)
- 数据长度自动计算(
postData.size()
)
安全机制:
- 使用
WINHTTP_FLAG_REFRESH
强制获取最新资源 - HTTPS请求自动忽略证书错误(测试环境适用)
5. 流式响应获取
int TryFetchResp(std::string& resp)
队列管理:
- 互斥锁保护的
_datas
队列 - 三态返回值设计:
>0
:有效数据长度0
:流结束标记-1
:暂无数据
性能优化:
- 队列首部数据优先出队(O(1)时间复杂度)
- 空队列时立即返回避免阻塞
6. 请求头设置
void SetHeaders(const std::unordered_map<std::string, std::string>& headers)
头处理机制:
- 使用
WINHTTP_ADDREQ_FLAG_COALESCE
自动合并重复头 - 支持批量添加/覆盖现有头
- 保留默认Content-Type设置
实现细节:
- 字符串转换使用UTF-8编码
- 头格式验证(自动添加冒号分隔符)
三、关键私有方法
1. 核心请求处理
int SendRequest(const std::wstring& method, ...)
多阶段处理:
- 连接建立:
WinHttpConnect
创建目标服务器连接 - 请求创建:
WinHttpOpenRequest
配置请求方法/路径 - 安全设置:HTTPS请求忽略证书错误
- 头注入:遍历
_headers
容器添加自定义头 - 数据发送:
WinHttpSendRequest
传输请求体 - 响应处理:同步模式直接读取,异步模式启动接收线程
异常处理:
- 每个WinAPI调用后立即检查返回值
- 使用结构化异常处理(SEH)捕获系统级错误
2. 流式接收器
void StreamReceiver(HINTERNET hConnect, HINTERNET hRequest)
并发设计:
- 使用原子变量
_streamActive
控制循环 - 独立线程持续调用
WinHttpReadData
- 数据块即时推入线程安全队列
资源管理:
- 函数退出时自动关闭连接/请求句柄
- 异常传播前确保资源释放
3. 字符串转换
std::string wstring2s(const std::wstring& wstr)
std::wstring string2w(const std::string& str)
编码转换:
- 基于Windows API的UTF-8转换
- 预分配目标缓冲区优化性能
- 空字符串快速路径
四、辅助函数解析
1. 全局替换函数
std::string ReplaceAll(std::string str, const std::string& from, const std::string& to)
实现特点:
- 原地修改(接收值拷贝)
- 处理重叠替换问题(pos递增策略)
- 空模式保护避免死循环
2. JSON内容提取
std::string ExtractContent(std::string& resp)
解析逻辑:
- 基于特征字符串定位(“content”:")
- 支持转义字符处理(\n, \r, ", \)
- 修改输入字符串实现流式解析
健壮性设计:
- 非规范JSON容错处理
- 内存安全(范围检查)
五、代码示例说明
1. 同步请求示例
SimpleHttp client("api.example.com", true);
std::string response;
client.Get("/data", response);
- 即时获取完整响应
- 适用于小型数据交互
2. 流式处理示例
client.Post("/stream", data, response, true);
while(int size = client.TryFetchResp(chunk)) {if(size > 0) process(chunk);
}
- 实时处理大文件/持续数据流
- 减少内存占用(无需完整缓存)
六、问题解决
Q:析构时触发断点
- 原因:流式线程未正确终止
- 解决方案:
~SimpleHttp() {CancelStream(); // 新增统一清理if(_hSession) WinHttpCloseHandle(_hSession); }void CancelStream() {if(_streamActive.exchange(false)) {WinHttpCloseHandle(_hRequest); // 强制终止if(_worker.joinable()) _worker.join();} }
Q:HTTPS证书错误
- 解决方案:生产环境应移除证书忽略配置
if(_isHttps) {DWORD dwFlags = SECURITY_FLAG_SECURE; // 严格验证WinHttpSetOption(...); }
七、总结
本实现通过封装WinHTTP API,在保持高性能的同时提供了简洁的接口。通过线程安全的流式处理、健壮的错误恢复和现代化的C++特性,适用于需要精细控制HTTP通信的Windows应用场景。未来可通过连接池、异步回调等机制进一步提升性能,满足更复杂的企业级需求。
八、完整代码
#include <windows.h>
#include <winhttp.h>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#include <tchar.h>
#include <mutex>
#include <atomic>
#include <thread>#pragma comment(lib, "winhttp.lib")
#include <strsafe.h>/// <summary>
/// 简单的HTTP类,自动支持http和https请求
/// 也支持流式处理,此时结果先缓存到队列,需主动调用TryFetchResp去轮询结果
/// </summary>
class SimpleHttp
{
public:SimpleHttp(const std::string& home, bool https = false) :_strBaseUrl(home), _isHttps(https){// 初始化WinHTTP会话 [[1]][[2]]_hSession = WinHttpOpen(L"SimpleHttp/1.0",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,WINHTTP_NO_PROXY_NAME,WINHTTP_NO_PROXY_BYPASS,0);if (!_hSession) {throw std::runtime_error("Failed to initialize WinHTTP session: " + GetLastError());}// 设置默认超时(5秒)DWORD timeout = 5000;WinHttpSetTimeouts(_hSession, timeout, timeout, timeout, timeout);}~SimpleHttp(){if (_worker.joinable()){_streamActive = false;_worker.join();}if (_hSession) WinHttpCloseHandle(_hSession);}/// <summary>/// 发起GET请求/// </summary>/// <param name="upath">接口名</param>/// <param name="resp">非流式请求时接收应答数据</param>/// <param name="stream">是否为流式请求</param>/// <returns></returns>int Get(const std::string& upath, std::string& resp, bool stream = false){return SendRequest(L"GET", upath, "", resp, stream);}/// <summary>/// /// </summary>/// <param name="upath">接口名</param>/// <param name="para">JSON请求参数</param>/// <param name="resp">非流式请求时接收应答数据</param>/// <param name="stream">是否为流式请求</param>/// <returns></returns>int Post(const std::string& upath, const std::string& para, std::string& resp, bool stream = false){return SendRequest(L"POST", upath, para, resp, stream);}/// <summary>/// 获取流式结果/// </summary>/// <param name="resp"></param>/// <returns>返回本次数据包大小,等于0表示数据包接收完成,-1表示没数据</returns>int TryFetchResp(std::string& resp){std::lock_guard<std::mutex> guard(_mtxDatas);if (_datas.empty()){return (_streamActive ? -1 : 0);}resp = _datas[0];_datas.erase(_datas.begin());return static_cast<int>(resp.size());}/// <summary>/// 设置请求头/// </summary>/// <param name="headers"></param>void SetHeaders(const std::unordered_map<std::string, std::string>& headers){for (auto& e : headers){_headers[e.first] = e.second;}}protected:/// <summary>/// 写入接收数据/// </summary>/// <param name="dat"></param>void Push(const std::string& dat){std::lock_guard<std::mutex> guard(_mtxDatas);if(!dat.empty()) _datas.push_back(dat);}private:int SendRequest(const std::wstring& method, const std::string& upath, const std::string& postData, std::string& resp, bool stream) {DWORD dwSize = 0;DWORD dwDownloaded = 0;LPSTR pszOutBuffer;BOOL bResults = FALSE;HINTERNET hConnect = nullptr;HINTERNET hRequest = nullptr;try {// 创建连接hConnect = WinHttpConnect(_hSession,string2w(_strBaseUrl).c_str(),INTERNET_DEFAULT_PORT,0);if (!hConnect) throw std::runtime_error("Connection failed");// 创建请求hRequest = WinHttpOpenRequest(hConnect,method.c_str(), string2w(upath).c_str(),nullptr, WINHTTP_NO_REFERER,WINHTTP_DEFAULT_ACCEPT_TYPES,(_isHttps ? (WINHTTP_FLAG_SECURE | WINHTTP_FLAG_REFRESH) : 0));if (!hRequest) throw std::runtime_error("Request creation failed");// 设置安全选项(忽略证书错误)if (_isHttps){DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA |SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, &dwFlags, sizeof(dwFlags));}// 设置请求头SetHeaders(hRequest);// 发送请求bResults = WinHttpSendRequest(hRequest,WINHTTP_NO_ADDITIONAL_HEADERS,0,(LPVOID)postData.c_str(),(DWORD)postData.size(),(DWORD)postData.size(),0);if (!bResults) throw std::runtime_error("Send request failed");// 接收响应if (!WinHttpReceiveResponse(hRequest, nullptr)){throw std::runtime_error("Receive response failed");}// 获取状态码 [[10]]DWORD dwStatusCode = 0;DWORD dwSize = sizeof(dwStatusCode);WinHttpQueryHeaders(hRequest,WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode,&dwSize,WINHTTP_NO_HEADER_INDEX);if (dwStatusCode != 200) {throw std::runtime_error("HTTP error: " + std::to_string(dwStatusCode));}// 流式子线程处理if (stream){_streamActive = true;_worker = std::thread(&SimpleHttp::StreamReceiver, this, hConnect, hRequest);return 0; // 立即返回}// 处理响应数据std::string buffer;do {dwSize = 0;if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize <= 0) break;size_t nRecv = dwSize;pszOutBuffer = new char[nRecv + 1];ZeroMemory(pszOutBuffer, nRecv + 1);if (WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)){buffer.append(pszOutBuffer, dwDownloaded);}delete[] pszOutBuffer;} while (dwSize > 0);resp = buffer;return 0;}catch (const std::exception& e) {if (hRequest) WinHttpCloseHandle(hRequest);if (hConnect) WinHttpCloseHandle(hConnect);throw std::runtime_error("WinHTTP Error: " + std::string(e.what()) +" [ErrorCode: " + std::to_string(GetLastError()) + "]");return -1;}}/// <summary>/// 流式接收应答/// </summary>void StreamReceiver(HINTERNET hConnect, HINTERNET hRequest){DWORD dwSize = 0;DWORD dwDownloaded = 0;char* pszOutBuffer = nullptr;std::string strErr;try {while (_streamActive) {dwSize = 0;if (!WinHttpQueryDataAvailable(hRequest, &dwSize) || dwSize == 0) break;const size_t nRecv = dwSize;pszOutBuffer = new char[nRecv + 1];ZeroMemory(pszOutBuffer, nRecv + 1);if (WinHttpReadData(hRequest, pszOutBuffer, dwSize, &dwDownloaded)){Push(std::string(pszOutBuffer, dwDownloaded));}delete[] pszOutBuffer;}}catch (...) {strErr = "stream recv data error";}// 发送结束标记_streamActive = false;WinHttpCloseHandle(hRequest);WinHttpCloseHandle(hConnect);if (!strErr.empty()){throw std::runtime_error(strErr.c_str());}}/// <summary>/// 设置请求头/// </summary>void SetHeaders(HINTERNET hRequest){// 创建请求后立即设置请求头for (const auto& h : _headers) {std::wstring header = string2w(h.first + ": " + h.second);if (!WinHttpAddRequestHeaders(hRequest,header.c_str(),static_cast<DWORD>(header.length()),WINHTTP_ADDREQ_FLAG_COALESCE)){//throw std::runtime_error("Failed to set header: " + h.first + " [Error: " + std::to_string(GetLastError()) + "]");}}}// std::wstring → std::string (UTF-8)std::string wstring2s(const std::wstring& wstr) {if (wstr.empty()) return {};int len = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);std::string result(len, '\0');WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), (LPSTR)result.data(), len, nullptr, nullptr);return result;}// std::string (UTF-8) → std::wstringstd::wstring string2w(const std::string& str) {if (str.empty()) return {};int len = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), nullptr, 0);std::wstring result(len, L'\0');MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), (LPWSTR)result.data(), len);return result;}private:// 是否httpsbool _isHttps{ false };// 基地址std::string _strBaseUrl;// 默认headersstd::unordered_map<std::string, std::string> _headers;// 异步数据std::vector<std::string> _datas;std::mutex _mtxDatas;// WinHttp对象HINTERNET _hSession = nullptr;// 流式操作子线程std::atomic<bool> _streamActive{ false };std::thread _worker;
};std::string ReplaceAll(std::string str, const std::string& from, const std::string& to) {if (from.empty()) return str; // 避免死循环[[2]]size_t pos = 0;while ((pos = str.find(from, pos)) != std::string::npos) {str.replace(pos, from.length(), to);pos += to.length();}return str;
}std::string ExtractContent(std::string& resp) {std::vector<std::string> results;std::string key = "\"content\":";size_t pos = 0;while ((pos = resp.find(key, pos)) != std::string::npos) {pos += key.length();// 跳过空白和冒号while (pos < resp.size() && (resp[pos] == ' ' || resp[pos] == ':')) pos++;if (resp[pos] != '"') continue; // 非字符串值跳过size_t start = ++pos;std::string content;// 处理转义字符和双引号while (pos < resp.size()) {char c = resp[pos++];if (c == '"') break;else if (c == '\\' && pos < resp.size()){switch (resp[pos++]){case 'n':c = '\n';break;case '\r':c = '\r';break;case '\\':c = '\\';break;case '"':c = '\"';break;default:pos--;break;}}content += c;}resp = resp.substr(pos);return content;}resp = "";return resp;
}int main()
{int iii = 0;std::cin >> iii;// 请求参数char para[4096] = "{\"stream\" : true,\"model\":\"deepseek-r1-distill-qwen-32b\",\"messages\":[{\"role\":\"user\",\"content\":\"Please help me write a C++ class for parsing XML, with user-friendly interfaces that support XPath and iterative data access.\"}]}";// char para[4096] = "{\"stream\" : true,\"model\":\"deepseek-r1-distill-qwen-32b\",\"messages\":[{\"role\":\"user\",\"content\":\"The prime numbers within 10?\"}]}";auto url = "https://cloud.infini-ai.com/maas/v1/chat/completions";auto api_key = "Bearer sk-************";auto model_name = "deepseek-r1-distill-qwen-32b";SimpleHttp web("cloud.infini-ai.com", true);web.SetHeaders({ { "Content-Type", "application/json" }, { "Authorization", "Bearer sk-daxdj5ksqdc6iuvn" } });std::string resp;web.Post("/maas/v1/chat/completions", para, resp, true);int nRecv = 0;do{nRecv = web.TryFetchResp(resp);if (nRecv > 0){while (!resp.empty()){std::cout << ExtractContent(resp);}}else{std::this_thread::sleep_for(std::chrono::milliseconds(100));}} while (nRecv);return 0;
}
运行结果: