updata:http

This commit is contained in:
2025-09-08 12:41:08 +08:00
parent 71f4b175ed
commit 2f21d8b948
4 changed files with 10431 additions and 9644 deletions

224
include/NetRequest.hpp Normal file
View File

@@ -0,0 +1,224 @@
/*
本文件
网络请求类需要实现以下功能:
1. 发送网络请求
2. 接收网络响应
3. 处理网络请求和响应
4. 实现网络请求和响应的回调函数
5. 实现网络请求和响应的错误处理
6. 实现网络请求和响应的日志记录
7. 实现网络请求和响应的性能统计
8. 实现网络请求和响应的并发控制
9. 实现网络请求和响应的缓存管理
10. 实现网络请求和响应的断点续传
11. 实现网络请求和响应的断点续传
12. 实现网络请求和响应的断点续传
13. 实现网络请求和响应的断点续传
14. 实现网络请求和响应的断点续传
*/
#pragma once
#include "httplib.h"
#include <string>
#include <functional>
#include <optional>
#include <future>
#include <chrono>
namespace ntq
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* @brief HTTP 响应对象
*
* 用于承载一次请求返回的状态码、正文与响应头;
* 当 from_cache 为 true 时,表示该响应来自本地缓存(而非实时网络)。
*/
struct HttpResponse
{
int status = 0; ///< HTTP 状态码(例如 200, 404 等)
std::string body; ///< 响应正文
httplib::Headers headers; ///< 响应头(大小写不敏感)
bool from_cache = false; ///< 是否来自缓存
};
/**
* @brief 错误码定义
*/
enum class ErrorCode
{
None = 0, ///< 无错误
Network, ///< 网络错误(连接失败/发送失败/接收失败等)
Timeout, ///< 超时
Canceled, ///< 被取消
InvalidURL, ///< URL 非法
IOError, ///< 本地 IO 错误(如写文件失败)
SSL, ///< SSL/HTTPS 相关错误
Unknown ///< 未分类错误
};
/**
* @brief 请求级别的参数配置
*
* 包含基础连接信息(协议、主机、端口)与默认头部、超时设置等。
*/
struct RequestOptions
{
std::string scheme = "http"; ///< 协议http 或 https
std::string host; ///< 目标主机名或 IP必填
int port = 80; ///< 端口http 默认为 80https 通常为 443
std::string base_path; ///< 可选的统一前缀(例如 "/api/v1"
int connect_timeout_ms = 5000; ///< 连接超时(毫秒)
int read_timeout_ms = 10000; ///< 读取超时(毫秒)
int write_timeout_ms = 10000; ///< 写入超时(毫秒)
bool keep_alive = true; ///< 是否保持连接Keep-Alive
httplib::Headers default_headers; ///< 默认头部,随所有请求发送
};
/**
* @class NetRequest
* @brief HTTP 客户端封装(基于 cpp-httplib
*
* 提供同步和异步的 GET/POST 请求、并发限制、可选缓存、日志回调、
* 性能统计以及断点续传下载到文件等能力。
*/
class NetRequest
{
public:
using LogCallback = std::function<void(const std::string &)>; ///< 日志回调类型
/**
* @brief 运行时统计数据
*/
struct Stats
{
uint64_t total_requests = 0; ///< 累计请求次数
uint64_t total_errors = 0; ///< 累计失败次数
double last_latency_ms = 0.0;///< 最近一次请求耗时(毫秒)
double avg_latency_ms = 0.0; ///< 指数平滑后的平均耗时(毫秒)
};
/**
* @brief 构造函数
* @param options 请求参数配置(目标地址、超时、默认头部等)
*/
explicit NetRequest(const RequestOptions &options);
/**
* @brief 析构函数
*/
~NetRequest();
/**
* @brief 设置日志回调
* @param logger 回调函数,接受一行日志字符串
*/
void setLogger(LogCallback logger);
/**
* @brief 设置最大并发请求数
* @param n 并发上限(最小值为 1
*/
void setMaxConcurrentRequests(size_t n);
/**
* @brief 启用内存缓存
* @param ttl 缓存有效期(超时后自动失效)
*/
void enableCache(std::chrono::milliseconds ttl);
/**
* @brief 禁用并清空缓存
*/
void disableCache();
/**
* @brief 发送同步 GET 请求
* @param path 资源路径(会与 base_path 合并)
* @param query 查询参数(会拼接为 ?k=v&...
* @param headers 额外请求头(与默认头部合并)
* @param err 可选错误码输出
* @return 成功返回响应对象,失败返回 std::nullopt
*/
std::optional<HttpResponse> Get(const std::string &path,
const httplib::Params &query = {},
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 发送同步 POST JSON 请求
* @param path 资源路径
* @param json JSON 字符串Content-Type: application/json
* @param headers 额外头部
* @param err 可选错误码输出
* @return 成功返回响应对象,失败返回 std::nullopt
*/
std::optional<HttpResponse> PostJson(const std::string &path,
const std::string &json,
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 发送同步 POST 表单请求application/x-www-form-urlencoded
* @param path 资源路径
* @param form 表单参数
* @param headers 额外头部
* @param err 可选错误码输出
* @return 成功返回响应对象,失败返回 std::nullopt
*/
std::optional<HttpResponse> PostForm(const std::string &path,
const httplib::Params &form,
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 异步 GET 请求
* @return std::future用于获取响应结果
*/
std::future<std::optional<HttpResponse>> GetAsync(const std::string &path,
const httplib::Params &query = {},
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 异步 POST JSON 请求
* @return std::future用于获取响应结果
*/
std::future<std::optional<HttpResponse>> PostJsonAsync(const std::string &path,
const std::string &json,
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 异步 POST 表单请求
* @return std::future用于获取响应结果
*/
std::future<std::optional<HttpResponse>> PostFormAsync(const std::string &path,
const httplib::Params &form,
const httplib::Headers &headers = {},
ErrorCode *err = nullptr);
/**
* @brief 下载文件到本地,支持断点续传
* @param path 资源路径
* @param local_file 本地保存路径
* @param headers 额外头部
* @param resume 是否启用续传Range
* @param chunk_size 预留参数:分块大小(当前由 httplib 内部回调驱动)
* @param err 可选错误码输出
* @return true 下载成功200 或 206false 失败
*/
bool DownloadToFile(const std::string &path,
const std::string &local_file,
const httplib::Headers &headers = {},
bool resume = true,
size_t chunk_size = 1 << 15,
ErrorCode *err = nullptr);
/**
* @brief 获取统计数据快照
*/
Stats getStats() const;
private:
struct Impl;
Impl *impl_;
};
}

563
src/NetRequest.cpp Normal file
View File

@@ -0,0 +1,563 @@
#include "NetRequest.hpp"
#include <mutex>
#include <condition_variable>
#include <unordered_map>
#include <fstream>
#include <sstream>
#include <atomic>
namespace ntq
{
namespace
{
static std::string joinPath(const std::string &base, const std::string &path)
{
if (base.empty()) return path.empty() || path[0] == '/' ? path : std::string("/") + path;
if (path.empty()) return base[0] == '/' ? base : std::string("/") + base;
bool base_has = base.front() == '/';
bool base_end = base.back() == '/';
bool path_has = path.front() == '/';
std::string b = base_has ? base : std::string("/") + base;
if (base_end && path_has) return b + path.substr(1);
if (!base_end && !path_has) return b + "/" + path;
return b + path;
}
static std::string paramsToQuery(const httplib::Params &params)
{
if (params.empty()) return {};
std::string s;
bool first = true;
for (auto &kv : params)
{
if (!first) s += '&';
first = false;
s += kv.first;
s += '=';
s += kv.second;
}
return s;
}
static httplib::Headers mergeHeaders(const httplib::Headers &a, const httplib::Headers &b)
{
httplib::Headers h = a;
for (auto &kv : b)
{
// 覆盖同名 header先删再插
h.erase(kv.first);
h.emplace(kv.first, kv.second);
}
return h;
}
}
class ConcurrencyGate
{
public:
explicit ConcurrencyGate(size_t limit) : limit_(limit), active_(0) {}
void set_limit(size_t limit)
{
std::lock_guard<std::mutex> lk(mtx_);
limit_ = limit > 0 ? limit : 1;
cv_.notify_all();
}
struct Guard
{
ConcurrencyGate &g;
explicit Guard(ConcurrencyGate &gate) : g(gate) { g.enter(); }
~Guard() { g.leave(); }
};
private:
friend struct Guard;
void enter()
{
std::unique_lock<std::mutex> lk(mtx_);
cv_.wait(lk, [&]{ return active_ < limit_; });
++active_;
}
void leave()
{
std::lock_guard<std::mutex> lk(mtx_);
if (active_ > 0) --active_;
cv_.notify_one();
}
size_t limit_;
size_t active_;
std::mutex mtx_;
std::condition_variable cv_;
};
struct NetRequest::Impl
{
RequestOptions opts;
LogCallback logger;
Stats stats;
// 并发控制
ConcurrencyGate gate{4};
// 缓存
struct CacheEntry
{
HttpResponse resp;
std::chrono::steady_clock::time_point expiry;
};
bool cache_enabled = false;
std::chrono::milliseconds cache_ttl{0};
std::unordered_map<std::string, CacheEntry> cache;
std::mutex cache_mtx;
void log(const std::string &msg)
{
if (logger) logger(msg);
}
template <typename ClientT>
void apply_client_options(ClientT &cli)
{
const time_t c_sec = static_cast<time_t>(opts.connect_timeout_ms / 1000);
const time_t c_usec = static_cast<time_t>((opts.connect_timeout_ms % 1000) * 1000);
const time_t r_sec = static_cast<time_t>(opts.read_timeout_ms / 1000);
const time_t r_usec = static_cast<time_t>((opts.read_timeout_ms % 1000) * 1000);
const time_t w_sec = static_cast<time_t>(opts.write_timeout_ms / 1000);
const time_t w_usec = static_cast<time_t>((opts.write_timeout_ms % 1000) * 1000);
cli.set_connection_timeout(c_sec, c_usec);
cli.set_read_timeout(r_sec, r_usec);
cli.set_write_timeout(w_sec, w_usec);
cli.set_keep_alive(opts.keep_alive);
}
std::string build_full_path(const std::string &path) const
{
return joinPath(opts.base_path, path);
}
std::string cache_key(const std::string &path, const httplib::Params &params, const httplib::Headers &headers)
{
std::ostringstream oss;
oss << opts.scheme << "://" << opts.host << ':' << opts.port << build_full_path(path);
if (!params.empty()) oss << '?' << paramsToQuery(params);
for (auto &kv : headers) oss << '|' << kv.first << '=' << kv.second;
return oss.str();
}
void record_latency(double ms)
{
stats.last_latency_ms = ms;
const double alpha = 0.2;
if (stats.avg_latency_ms <= 0.0) stats.avg_latency_ms = ms;
else stats.avg_latency_ms = alpha * ms + (1.0 - alpha) * stats.avg_latency_ms;
}
static ErrorCode map_error()
{
// 简化:无法区分具体错误码,统一归为 Network
return ErrorCode::Network;
}
};
NetRequest::NetRequest(const RequestOptions &options)
: impl_(new Impl)
{
impl_->opts = options;
if (impl_->opts.scheme == "https" && impl_->opts.port == 80) impl_->opts.port = 443;
if (impl_->opts.scheme == "http" && impl_->opts.port == 0) impl_->opts.port = 80;
}
NetRequest::~NetRequest()
{
delete impl_;
}
void NetRequest::setLogger(LogCallback logger)
{
impl_->logger = std::move(logger);
}
void NetRequest::setMaxConcurrentRequests(size_t n)
{
impl_->gate.set_limit(n > 0 ? n : 1);
}
void NetRequest::enableCache(std::chrono::milliseconds ttl)
{
impl_->cache_enabled = true;
impl_->cache_ttl = ttl.count() > 0 ? ttl : std::chrono::milliseconds(1000);
}
void NetRequest::disableCache()
{
impl_->cache_enabled = false;
std::lock_guard<std::mutex> lk(impl_->cache_mtx);
impl_->cache.clear();
}
std::optional<HttpResponse> NetRequest::Get(const std::string &path,
const httplib::Params &query,
const httplib::Headers &headers,
ErrorCode *err)
{
ConcurrencyGate::Guard guard(impl_->gate);
impl_->stats.total_requests++;
auto start = std::chrono::steady_clock::now();
if (impl_->cache_enabled)
{
std::string key = impl_->cache_key(path, query, mergeHeaders(impl_->opts.default_headers, headers));
std::lock_guard<std::mutex> lk(impl_->cache_mtx);
auto it = impl_->cache.find(key);
if (it != impl_->cache.end() && std::chrono::steady_clock::now() < it->second.expiry)
{
if (err) *err = ErrorCode::None;
auto resp = it->second.resp;
resp.from_cache = true;
return resp;
}
}
std::optional<HttpResponse> result;
ErrorCode local_err = ErrorCode::None;
const auto full_path = impl_->build_full_path(path);
auto merged_headers = mergeHeaders(impl_->opts.default_headers, headers);
if (impl_->opts.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
httplib::SSLClient cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = query.empty() ? cli.Get(full_path.c_str(), merged_headers)
: cli.Get(full_path.c_str(), query, merged_headers);
if (res)
{
HttpResponse r;
r.status = res->status;
r.body = res->body;
r.headers = res->headers;
r.from_cache = false;
result = r;
}
else
{
local_err = Impl::map_error();
}
#else
impl_->log("HTTPS requested but OpenSSL is not enabled; falling back to error.");
local_err = ErrorCode::SSL;
#endif
}
else
{
httplib::Client cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = query.empty() ? cli.Get(full_path.c_str(), merged_headers)
: cli.Get(full_path.c_str(), query, merged_headers);
if (res)
{
HttpResponse r;
r.status = res->status;
r.body = res->body;
r.headers = res->headers;
r.from_cache = false;
result = r;
}
else
{
local_err = Impl::map_error();
}
}
auto end = std::chrono::steady_clock::now();
impl_->record_latency(std::chrono::duration<double, std::milli>(end - start).count());
if (!result.has_value())
{
impl_->stats.total_errors++;
if (err) *err = local_err;
return std::nullopt;
}
if (impl_->cache_enabled)
{
std::string key = impl_->cache_key(path, query, merged_headers);
std::lock_guard<std::mutex> lk(impl_->cache_mtx);
impl_->cache[key] = Impl::CacheEntry{*result, std::chrono::steady_clock::now() + impl_->cache_ttl};
}
if (err) *err = ErrorCode::None;
return result;
}
std::optional<HttpResponse> NetRequest::PostJson(const std::string &path,
const std::string &json,
const httplib::Headers &headers,
ErrorCode *err)
{
ConcurrencyGate::Guard guard(impl_->gate);
impl_->stats.total_requests++;
auto start = std::chrono::steady_clock::now();
std::optional<HttpResponse> result;
ErrorCode local_err = ErrorCode::None;
const auto full_path = impl_->build_full_path(path);
auto merged_headers = mergeHeaders(impl_->opts.default_headers, headers);
if (impl_->opts.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
httplib::SSLClient cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Post(full_path.c_str(), merged_headers, json, "application/json");
if (res)
{
HttpResponse r{res->status, res->body, res->headers, false};
result = r;
}
else
{
local_err = Impl::map_error();
}
#else
local_err = ErrorCode::SSL;
#endif
}
else
{
httplib::Client cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Post(full_path.c_str(), merged_headers, json, "application/json");
if (res)
{
HttpResponse r{res->status, res->body, res->headers, false};
result = r;
}
else
{
local_err = Impl::map_error();
}
}
auto end = std::chrono::steady_clock::now();
impl_->record_latency(std::chrono::duration<double, std::milli>(end - start).count());
if (!result)
{
impl_->stats.total_errors++;
if (err) *err = local_err;
return std::nullopt;
}
if (err) *err = ErrorCode::None;
return result;
}
std::optional<HttpResponse> NetRequest::PostForm(const std::string &path,
const httplib::Params &form,
const httplib::Headers &headers,
ErrorCode *err)
{
ConcurrencyGate::Guard guard(impl_->gate);
impl_->stats.total_requests++;
auto start = std::chrono::steady_clock::now();
std::optional<HttpResponse> result;
ErrorCode local_err = ErrorCode::None;
const auto full_path = impl_->build_full_path(path);
auto merged_headers = mergeHeaders(impl_->opts.default_headers, headers);
if (impl_->opts.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
httplib::SSLClient cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Post(full_path.c_str(), merged_headers, form);
if (res)
{
HttpResponse r{res->status, res->body, res->headers, false};
result = r;
}
else
{
local_err = Impl::map_error();
}
#else
local_err = ErrorCode::SSL;
#endif
}
else
{
httplib::Client cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Post(full_path.c_str(), merged_headers, form);
if (res)
{
HttpResponse r{res->status, res->body, res->headers, false};
result = r;
}
else
{
local_err = Impl::map_error();
}
}
auto end = std::chrono::steady_clock::now();
impl_->record_latency(std::chrono::duration<double, std::milli>(end - start).count());
if (!result)
{
impl_->stats.total_errors++;
if (err) *err = local_err;
return std::nullopt;
}
if (err) *err = ErrorCode::None;
return result;
}
std::future<std::optional<HttpResponse>> NetRequest::GetAsync(const std::string &path,
const httplib::Params &query,
const httplib::Headers &headers,
ErrorCode *err)
{
return std::async(std::launch::async, [this, path, query, headers, err]() mutable {
ErrorCode local;
auto r = Get(path, query, headers, &local);
if (err) *err = local;
return r;
});
}
std::future<std::optional<HttpResponse>> NetRequest::PostJsonAsync(const std::string &path,
const std::string &json,
const httplib::Headers &headers,
ErrorCode *err)
{
return std::async(std::launch::async, [this, path, json, headers, err]() mutable {
ErrorCode local;
auto r = PostJson(path, json, headers, &local);
if (err) *err = local;
return r;
});
}
std::future<std::optional<HttpResponse>> NetRequest::PostFormAsync(const std::string &path,
const httplib::Params &form,
const httplib::Headers &headers,
ErrorCode *err)
{
return std::async(std::launch::async, [this, path, form, headers, err]() mutable {
ErrorCode local;
auto r = PostForm(path, form, headers, &local);
if (err) *err = local;
return r;
});
}
bool NetRequest::DownloadToFile(const std::string &path,
const std::string &local_file,
const httplib::Headers &headers,
bool resume,
size_t /*chunk_size*/,
ErrorCode *err)
{
ConcurrencyGate::Guard guard(impl_->gate);
impl_->stats.total_requests++;
auto start = std::chrono::steady_clock::now();
std::ios_base::openmode mode = std::ios::binary | std::ios::out;
size_t offset = 0;
if (resume)
{
std::ifstream in(local_file, std::ios::binary | std::ios::ate);
if (in)
{
offset = static_cast<size_t>(in.tellg());
}
mode |= std::ios::app;
}
else
{
mode |= std::ios::trunc;
}
std::ofstream out(local_file, mode);
if (!out)
{
if (err) *err = ErrorCode::IOError;
impl_->stats.total_errors++;
return false;
}
auto merged_headers = mergeHeaders(impl_->opts.default_headers, headers);
if (resume && offset > 0)
{
merged_headers.emplace("Range", "bytes=" + std::to_string(offset) + "-");
}
const auto full_path = impl_->build_full_path(path);
int status_code = 0;
ErrorCode local_err = ErrorCode::None;
auto content_receiver = [&](const char *data, size_t data_length) {
out.write(data, static_cast<std::streamsize>(data_length));
return static_cast<bool>(out);
};
bool ok = false;
if (impl_->opts.scheme == "https")
{
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
httplib::SSLClient cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Get(full_path.c_str(), merged_headers, content_receiver);
if (res)
{
status_code = res->status;
ok = (status_code == 200 || status_code == 206);
}
else
{
local_err = Impl::map_error();
}
#else
local_err = ErrorCode::SSL;
#endif
}
else
{
httplib::Client cli(impl_->opts.host.c_str(), impl_->opts.port);
impl_->apply_client_options(cli);
auto res = cli.Get(full_path.c_str(), merged_headers, content_receiver);
if (res)
{
status_code = res->status;
ok = (status_code == 200 || status_code == 206);
}
else
{
local_err = Impl::map_error();
}
}
out.close();
auto end = std::chrono::steady_clock::now();
impl_->record_latency(std::chrono::duration<double, std::milli>(end - start).count());
if (!ok)
{
impl_->stats.total_errors++;
if (err) *err = local_err;
return false;
}
if (err) *err = ErrorCode::None;
return true;
}
NetRequest::Stats NetRequest::getStats() const
{
return impl_->stats;
}
}