Files
Netralib/include/NetRequest.hpp
2025-09-08 15:12:25 +08:00

344 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
本文件
网络请求类需要实现以下功能:
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>
// 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
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
*/
ntq::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
*/
ntq::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
*/
ntq::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<ntq::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<ntq::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<ntq::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;
/**
* @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);
private:
struct Impl;
Impl *impl_;
};
}