2025-09-08 12:41:08 +08:00
|
|
|
|
/*
|
|
|
|
|
本文件
|
|
|
|
|
网络请求类需要实现以下功能:
|
|
|
|
|
1. 发送网络请求
|
|
|
|
|
2. 接收网络响应
|
|
|
|
|
3. 处理网络请求和响应
|
|
|
|
|
4. 实现网络请求和响应的回调函数
|
|
|
|
|
5. 实现网络请求和响应的错误处理
|
|
|
|
|
6. 实现网络请求和响应的日志记录
|
|
|
|
|
7. 实现网络请求和响应的性能统计
|
|
|
|
|
8. 实现网络请求和响应的并发控制
|
|
|
|
|
9. 实现网络请求和响应的缓存管理
|
|
|
|
|
10. 实现网络请求和响应的断点续传
|
|
|
|
|
11. 实现网络请求和响应的断点续传
|
|
|
|
|
12. 实现网络请求和响应的断点续传
|
|
|
|
|
13. 实现网络请求和响应的断点续传
|
|
|
|
|
14. 实现网络请求和响应的断点续传
|
|
|
|
|
*/
|
|
|
|
|
#pragma once
|
|
|
|
|
#include "httplib.h"
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <functional>
|
|
|
|
|
#include <future>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
|
2025-09-08 15:12:25 +08:00
|
|
|
|
// C++17/14 可选类型回退适配:统一使用 ntq::optional / ntq::nullopt
|
|
|
|
|
#if defined(__has_include)
|
|
|
|
|
#if __has_include(<optional>)
|
|
|
|
|
#include <optional>
|
|
|
|
|
// 仅当启用了 C++17 或库声明了 optional 功能时,才使用 std::optional
|
|
|
|
|
#if defined(__cpp_lib_optional) || (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
|
|
|
|
|
namespace ntq { template <typename T> using optional = std::optional<T>; }
|
|
|
|
|
namespace ntq { constexpr auto nullopt = std::nullopt; using nullopt_t = decltype(std::nullopt); }
|
|
|
|
|
#elif __has_include(<experimental/optional>)
|
|
|
|
|
#include <experimental/optional>
|
|
|
|
|
namespace ntq { template <typename T> using optional = std::experimental::optional<T>; }
|
|
|
|
|
namespace ntq { constexpr auto nullopt = std::experimental::nullopt; using nullopt_t = decltype(std::experimental::nullopt); }
|
|
|
|
|
#else
|
|
|
|
|
#include <utility>
|
|
|
|
|
namespace ntq {
|
|
|
|
|
struct nullopt_t { explicit constexpr nullopt_t(int) {} };
|
|
|
|
|
static constexpr nullopt_t nullopt{0};
|
|
|
|
|
template <typename T>
|
|
|
|
|
class optional {
|
|
|
|
|
public:
|
|
|
|
|
optional() : has_(false) {}
|
|
|
|
|
optional(nullopt_t) : has_(false) {}
|
|
|
|
|
optional(const T &v) : has_(true), value_(v) {}
|
|
|
|
|
optional(T &&v) : has_(true), value_(std::move(v)) {}
|
|
|
|
|
optional(const optional &o) : has_(o.has_), value_(o.has_ ? o.value_ : T{}) {}
|
|
|
|
|
optional(optional &&o) noexcept : has_(o.has_), value_(std::move(o.value_)) {}
|
|
|
|
|
optional &operator=(nullopt_t) { has_ = false; return *this; }
|
|
|
|
|
optional &operator=(const T &v) { value_ = v; has_ = true; return *this; }
|
|
|
|
|
optional &operator=(T &&v) { value_ = std::move(v); has_ = true; return *this; }
|
|
|
|
|
explicit operator bool() const { return has_; }
|
|
|
|
|
bool has_value() const { return has_; }
|
|
|
|
|
T &value() { return value_; }
|
|
|
|
|
const T &value() const { return value_; }
|
|
|
|
|
T &operator*() { return value_; }
|
|
|
|
|
const T &operator*() const { return value_; }
|
|
|
|
|
private:
|
|
|
|
|
bool has_ = false;
|
|
|
|
|
T value_{};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
#elif __has_include(<experimental/optional>)
|
|
|
|
|
#include <experimental/optional>
|
|
|
|
|
namespace ntq { template <typename T> using optional = std::experimental::optional<T>; }
|
|
|
|
|
namespace ntq { constexpr auto nullopt = std::experimental::nullopt; using nullopt_t = decltype(std::experimental::nullopt); }
|
|
|
|
|
#else
|
|
|
|
|
#include <utility>
|
|
|
|
|
namespace ntq {
|
|
|
|
|
struct nullopt_t { explicit constexpr nullopt_t(int) {} };
|
|
|
|
|
static constexpr nullopt_t nullopt{0};
|
|
|
|
|
template <typename T>
|
|
|
|
|
class optional {
|
|
|
|
|
public:
|
|
|
|
|
optional() : has_(false) {}
|
|
|
|
|
optional(nullopt_t) : has_(false) {}
|
|
|
|
|
optional(const T &v) : has_(true), value_(v) {}
|
|
|
|
|
optional(T &&v) : has_(true), value_(std::move(v)) {}
|
|
|
|
|
optional(const optional &o) : has_(o.has_), value_(o.has_ ? o.value_ : T{}) {}
|
|
|
|
|
optional(optional &&o) noexcept : has_(o.has_), value_(std::move(o.value_)) {}
|
|
|
|
|
optional &operator=(nullopt_t) { has_ = false; return *this; }
|
|
|
|
|
optional &operator=(const T &v) { value_ = v; has_ = true; return *this; }
|
|
|
|
|
optional &operator=(T &&v) { value_ = std::move(v); has_ = true; return *this; }
|
|
|
|
|
explicit operator bool() const { return has_; }
|
|
|
|
|
bool has_value() const { return has_; }
|
|
|
|
|
T &value() { return value_; }
|
|
|
|
|
const T &value() const { return value_; }
|
|
|
|
|
T &operator*() { return value_; }
|
|
|
|
|
const T &operator*() const { return value_; }
|
|
|
|
|
private:
|
|
|
|
|
bool has_ = false;
|
|
|
|
|
T value_{};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
#else
|
|
|
|
|
// 无 __has_include:按语言级别判断
|
|
|
|
|
#if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
|
|
|
|
|
#include <optional>
|
|
|
|
|
namespace ntq { template <typename T> using optional = std::optional<T>; }
|
|
|
|
|
namespace ntq { constexpr auto nullopt = std::nullopt; using nullopt_t = decltype(std::nullopt); }
|
|
|
|
|
#else
|
|
|
|
|
#include <experimental/optional>
|
|
|
|
|
namespace ntq { template <typename T> using optional = std::experimental::optional<T>; }
|
|
|
|
|
namespace ntq { constexpr auto nullopt = std::experimental::nullopt; using nullopt_t = decltype(std::experimental::nullopt); }
|
|
|
|
|
#endif
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-09-08 12:41:08 +08:00
|
|
|
|
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 默认为 80,https 通常为 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
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
ntq::optional<HttpResponse> Get(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
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
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
ntq::optional<HttpResponse> PostJson(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
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
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
ntq::optional<HttpResponse> PostForm(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
const httplib::Params &form,
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief 异步 GET 请求
|
|
|
|
|
* @return std::future,用于获取响应结果
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
std::future<ntq::optional<HttpResponse>> GetAsync(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
const httplib::Params &query = {},
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief 异步 POST JSON 请求
|
|
|
|
|
* @return std::future,用于获取响应结果
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
std::future<ntq::optional<HttpResponse>> PostJsonAsync(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
const std::string &json,
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief 异步 POST 表单请求
|
|
|
|
|
* @return std::future,用于获取响应结果
|
|
|
|
|
*/
|
2025-09-08 15:12:25 +08:00
|
|
|
|
std::future<ntq::optional<HttpResponse>> PostFormAsync(const std::string &path,
|
2025-09-08 12:41:08 +08:00
|
|
|
|
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 或 206),false 失败
|
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
|
2025-09-08 15:12:25 +08:00
|
|
|
|
/**
|
|
|
|
|
* @brief 便捷:直接用完整 URL 发起 GET(无需显式实例化)
|
|
|
|
|
* @param url 形如 http://host:port/path?x=1 或 https://host/path
|
|
|
|
|
* @param headers 额外头部
|
|
|
|
|
* @param err 可选错误码输出
|
|
|
|
|
*/
|
|
|
|
|
static ntq::optional<HttpResponse> QuickGet(const std::string &url,
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief 便捷:直接用完整 URL 发起 POST JSON(无需显式实例化)
|
|
|
|
|
* @param url 形如 http://host:port/path?x=1 或 https://host/path
|
|
|
|
|
* @param json JSON 字符串(Content-Type: application/json)
|
|
|
|
|
* @param headers 额外头部
|
|
|
|
|
* @param err 可选错误码输出
|
|
|
|
|
*/
|
|
|
|
|
static ntq::optional<HttpResponse> QuickPostJson(const std::string &url,
|
|
|
|
|
const std::string &json,
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief 便捷:直接用完整 URL 发起 POST 表单(无需显式实例化)
|
|
|
|
|
* @param url 形如 http://host:port/path?x=1 或 https://host/path
|
|
|
|
|
* @param form 表单参数
|
|
|
|
|
* @param headers 额外头部
|
|
|
|
|
* @param err 可选错误码输出
|
|
|
|
|
*/
|
|
|
|
|
static ntq::optional<HttpResponse> QuickPostForm(const std::string &url,
|
|
|
|
|
const httplib::Params &form,
|
|
|
|
|
const httplib::Headers &headers = {},
|
|
|
|
|
ErrorCode *err = nullptr);
|
|
|
|
|
|
2025-09-08 12:41:08 +08:00
|
|
|
|
private:
|
|
|
|
|
struct Impl;
|
|
|
|
|
Impl *impl_;
|
|
|
|
|
};
|
|
|
|
|
}
|