Compare commits
10 Commits
3a2ffedcbb
...
2f21d8b948
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f21d8b948 | |||
|
|
71f4b175ed | ||
|
|
c8ce637315 | ||
|
|
9bfc4c2538 | ||
|
|
8ed879cd90 | ||
|
|
72a82ccac2 | ||
|
|
140decf42e | ||
|
|
f3c7d97edf | ||
|
|
1683996d43 | ||
|
|
7ee0b69aa6 |
10
README.md
10
README.md
@@ -11,8 +11,9 @@ c/c++基本开发库
|
||||
# 写文件操作
|
||||
允许原文本进行覆盖写,追加写
|
||||
允许二进制进行覆盖写,追加写
|
||||
允许在特定位置进行写操作
|
||||
可以计算特定符号最后一个字节所在位置
|
||||
允许在特定位置后面进行插入覆盖操作
|
||||
允许删除特定字段后面所有内容在进行写操作
|
||||
可以根据需要计算特定符号最后一个字节或者第一个字节所在位置所在位置
|
||||
|
||||
所有操作都添加mutex锁机制 ,保障线程安全
|
||||
|
||||
@@ -28,3 +29,8 @@ c/c++基本开发库
|
||||
所有操作都添加mutex锁机制 ,保障线程安全
|
||||
|
||||
|
||||
# 字符串操作
|
||||
支持左右空格删除
|
||||
支持格式化输出
|
||||
|
||||
|
||||
|
||||
224
include/NetRequest.hpp
Normal file
224
include/NetRequest.hpp
Normal 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 默认为 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
|
||||
*/
|
||||
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 或 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;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl *impl_;
|
||||
};
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace QCL
|
||||
* @param clientSock 客户端Socket描述符
|
||||
* @param flag:false 非阻塞模式,true 阻塞模式
|
||||
*/
|
||||
char *receiveFromClient(int clientSock, bool flag = true);
|
||||
std::string receiveFromClient(int clientSock, bool flag = true);
|
||||
|
||||
/**
|
||||
* @brief 获取连接客户端的IP和端口
|
||||
@@ -120,19 +120,6 @@ namespace QCL
|
||||
*/
|
||||
bool appendText(const std::string &content);
|
||||
|
||||
/**
|
||||
* @brief 按指定位置写入文本(原文写,线程安全)
|
||||
*
|
||||
* 不清空文件内容,仅替换指定位置的内容。
|
||||
* 若文件不存在,则会自动创建空文件。
|
||||
*
|
||||
* @param content 要写入的内容
|
||||
* @param position 写入位置(默认为文件开头)
|
||||
* @return true 写入成功
|
||||
* @return false 写入失败
|
||||
*/
|
||||
bool writeOriginal(const std::string &content, std::streampos position = 0);
|
||||
|
||||
/**
|
||||
* @brief 覆盖写二进制文件(线程安全)
|
||||
* @param data 要写入的二进制数据
|
||||
@@ -150,15 +137,64 @@ namespace QCL
|
||||
bool appendBinary(const std::vector<char> &data);
|
||||
|
||||
/**
|
||||
* @brief 计算第一个指定字节序列前的字节数(包含该字节序列本身)
|
||||
*
|
||||
* 例如:文件内容是 "ABC***--***XYZ",模式是 "***--***"
|
||||
* 返回值应为 11("ABC"=3字节 + "***--***"=8字节)。
|
||||
*
|
||||
* @param pattern 要查找的字节模式(支持多字节)
|
||||
* @return long 字节数(包含匹配模式本身),未找到返回 -1
|
||||
* @brief 计算第一个指定字节序列前的字节数
|
||||
* @param pattern 要查找的字节序列
|
||||
* @param includePattern true 表示返回值包含 pattern 自身长度,false 表示不包含
|
||||
* @return size_t 字节数,如果文件没打开或 pattern 为空则返回 0
|
||||
*/
|
||||
long countBytesBeforePattern(const std::string &pattern);
|
||||
size_t countBytesPattern(const std::string &pattern, bool includePattern = false);
|
||||
|
||||
/**
|
||||
* @brief 在文件中查找指定字节序列并在其后写入内容,如果不存在则追加到文件末尾
|
||||
* @param pattern 要查找的字节序列
|
||||
* @param content 要写入的内容
|
||||
* @return true 写入成功,false 文件打开失败
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 若文件中存在 pattern,则删除 pattern 之后的所有内容,并在其后插入 content。
|
||||
* 2. 若文件中不存在 pattern,则在文件末尾追加 content,若末尾无换行符则先补充换行。
|
||||
*/
|
||||
bool writeAfterPatternOrAppend(const std::string &pattern, const std::string &content);
|
||||
|
||||
/**
|
||||
* @brief 在文件指定位置之后插入内容
|
||||
* @param content 要插入的内容
|
||||
* @param pos 插入位置(从文件开头算起的字节偏移量)
|
||||
* @param length 插入的长度(>= content.size() 时,多余部分用空字节填充;< content.size() 时只截取前 length 个字节)
|
||||
* @return true 插入成功,false 文件打开失败或参数不合法
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 不会覆盖原有数据,而是将 pos 之后的内容整体向后移动 length 个字节。
|
||||
* 2. 如果 length > content.size(),则在 content 后补充 '\0'(或空格,可按需求改)。
|
||||
* 3. 如果 length < content.size(),则只写入 content 的前 length 个字节。
|
||||
* 4. 文件整体大小会增加 length 个字节。
|
||||
*
|
||||
* 举例:
|
||||
* 原始文件内容: "ABCDEFG"
|
||||
* insertAfterPos("XY", 2, 3) // 在索引 2 后插入
|
||||
* 结果: "ABX Y\0CDEFG" (这里 \0 代表补充的空字节)
|
||||
*/
|
||||
bool insertAfterPos(const std::string &content, size_t pos, size_t length);
|
||||
|
||||
/**
|
||||
* @brief 在文件指定位置覆盖写入内容
|
||||
* @param content 要写入的内容
|
||||
* @param pos 覆盖起始位置(从文件开头算起的字节偏移量)
|
||||
* @param length 覆盖长度
|
||||
* @return true 覆盖成功,false 文件打开失败或 pos 越界
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 从 pos 开始覆盖 length 个字节,不会移动或增加文件大小。
|
||||
* 2. 如果 content.size() >= length,则只写入前 length 个字节。
|
||||
* 3. 如果 content.size() < length,则写入 content,并用 '\0' 补齐至 length。
|
||||
* 4. 如果 pos + length 超过文件末尾,则只覆盖到文件尾部,不会越界。
|
||||
*
|
||||
* 举例:
|
||||
* 原始文件内容: "ABCDEFG"
|
||||
* overwriteAtPos("XY", 2, 3)
|
||||
* 结果: "ABXYEFG" (原 "CDE" 被 "XY\0" 覆盖,\0 实际不可见)
|
||||
*/
|
||||
bool overwriteAtPos(const std::string &content, size_t pos, size_t length);
|
||||
|
||||
private:
|
||||
std::string filePath_; ///< 文件路径
|
||||
@@ -259,9 +295,17 @@ namespace QCL
|
||||
/**
|
||||
* @brief 获取指定字节序列前的字节数(包含该字节序列)
|
||||
* @param marker 要查找的字节序列(可能不止一个字节)
|
||||
* @return 如果找到,返回前面部分字节数;找不到返回全文字节数
|
||||
* @return 如果找到,返回前面部分字节数;找不到返回0
|
||||
*/
|
||||
size_t GetBytesBefore(const std::string &marker);
|
||||
size_t GetBytesBefore(const std::string &marker, bool includeMarker = false);
|
||||
|
||||
/**
|
||||
* @brief 从指定位置读取指定字节数,默认读取到文件末尾
|
||||
* @param pos 起始位置(字节偏移)
|
||||
* @param count 要读取的字节数,默认为0表示读取到文件末尾
|
||||
* @return 读取到的字节数据
|
||||
*/
|
||||
std::vector<char> ReadBytesFrom(size_t pos, size_t count = 0);
|
||||
|
||||
/**
|
||||
* @brief 检查文件是否存在
|
||||
@@ -287,4 +331,81 @@ namespace QCL
|
||||
|
||||
// 屏蔽所有信号
|
||||
void blockAllSignals();
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 字符串操作
|
||||
// 去除字符串的左空格
|
||||
std::string Ltrim(const std::string &s);
|
||||
|
||||
// 去除字符串右侧的空格
|
||||
std::string Rtrim(const std::string &s);
|
||||
|
||||
// 去除字符串左右两侧的空格
|
||||
std::string LRtrim(const std::string &s);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// c++进行格式化输出
|
||||
// 通用类型转字符串
|
||||
template <typename T>
|
||||
std::string to_string_any(const T &value)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// 递归获取 tuple 中 index 对应参数
|
||||
template <std::size_t I = 0, typename Tuple>
|
||||
std::string get_tuple_arg(const Tuple &tup, std::size_t index)
|
||||
{
|
||||
if constexpr (I < std::tuple_size_v<Tuple>)
|
||||
{
|
||||
if (I == index)
|
||||
return to_string_any(std::get<I>(tup));
|
||||
else
|
||||
return get_tuple_arg<I + 1>(tup, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Too few arguments for format string");
|
||||
}
|
||||
}
|
||||
|
||||
// format 函数
|
||||
template <typename... Args>
|
||||
std::string format(const std::string &fmt, const Args &...args)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
std::tuple<const Args &...> tup(args...);
|
||||
size_t pos = 0;
|
||||
size_t arg_idx = 0;
|
||||
|
||||
while (pos < fmt.size())
|
||||
{
|
||||
if (fmt[pos] == '{' && pos + 1 < fmt.size() && fmt[pos + 1] == '{')
|
||||
{
|
||||
oss << '{';
|
||||
pos += 2;
|
||||
}
|
||||
else if (fmt[pos] == '}' && pos + 1 < fmt.size() && fmt[pos + 1] == '}')
|
||||
{
|
||||
oss << '}';
|
||||
pos += 2;
|
||||
}
|
||||
else if (fmt[pos] == '{' && pos + 1 < fmt.size() && fmt[pos + 1] == '}')
|
||||
{
|
||||
oss << get_tuple_arg(tup, arg_idx++);
|
||||
pos += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
oss << fmt[pos++];
|
||||
}
|
||||
}
|
||||
|
||||
if (arg_idx < sizeof...(Args))
|
||||
throw std::runtime_error("Too many arguments for format string");
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <iomanip> // 流格式控制(setw/setprecision)
|
||||
#include <regex> // 正则表达式
|
||||
#include <filesystem> // 文件系统(C++17)
|
||||
#include<termios.h>
|
||||
|
||||
// ==================== 并发编程支持 ====================
|
||||
#include <thread> // 线程管理(std::thread)
|
||||
|
||||
850
include/README.md
Normal file
850
include/README.md
Normal file
@@ -0,0 +1,850 @@
|
||||
cpp-httplib
|
||||
===========
|
||||
|
||||
[](https://github.com/yhirose/cpp-httplib/actions)
|
||||
|
||||
A C++11 single-file header-only cross platform HTTP/HTTPS library.
|
||||
|
||||
It's extremely easy to setup. Just include the **httplib.h** file in your code!
|
||||
|
||||
NOTE: This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want.
|
||||
|
||||
Simple examples
|
||||
---------------
|
||||
|
||||
#### Server (Multi-threaded)
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
// HTTP
|
||||
httplib::Server svr;
|
||||
|
||||
// HTTPS
|
||||
httplib::SSLServer svr;
|
||||
|
||||
svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
```
|
||||
|
||||
#### Client
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
// HTTP
|
||||
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
|
||||
|
||||
// HTTPS
|
||||
httplib::Client cli("https://cpp-httplib-server.yhirose.repl.co");
|
||||
|
||||
auto res = cli.Get("/hi");
|
||||
res->status;
|
||||
res->body;
|
||||
```
|
||||
|
||||
SSL Support
|
||||
-----------
|
||||
|
||||
SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked.
|
||||
|
||||
NOTE: cpp-httplib currently supports only version 1.1.1 and 3.0.
|
||||
|
||||
NOTE for macOS: cpp-httplib now can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`.
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
// Server
|
||||
httplib::SSLServer svr("./cert.pem", "./key.pem");
|
||||
|
||||
// Client
|
||||
httplib::Client cli("https://localhost:1234"); // scheme + host
|
||||
httplib::SSLClient cli("localhost:1234"); // host
|
||||
httplib::SSLClient cli("localhost", 1234); // host, port
|
||||
|
||||
// Use your CA bundle
|
||||
cli.set_ca_cert_path("./ca-bundle.crt");
|
||||
|
||||
// Disable cert verification
|
||||
cli.enable_server_certificate_verification(false);
|
||||
```
|
||||
|
||||
NOTE: When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
|
||||
|
||||
Server
|
||||
------
|
||||
|
||||
```c++
|
||||
#include <httplib.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
using namespace httplib;
|
||||
|
||||
Server svr;
|
||||
|
||||
svr.Get("/hi", [](const Request& req, Response& res) {
|
||||
res.set_content("Hello World!", "text/plain");
|
||||
});
|
||||
|
||||
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
|
||||
auto numbers = req.matches[1];
|
||||
res.set_content(numbers, "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/body-header-param", [](const Request& req, Response& res) {
|
||||
if (req.has_header("Content-Length")) {
|
||||
auto val = req.get_header_value("Content-Length");
|
||||
}
|
||||
if (req.has_param("key")) {
|
||||
auto val = req.get_param_value("key");
|
||||
}
|
||||
res.set_content(req.body, "text/plain");
|
||||
});
|
||||
|
||||
svr.Get("/stop", [&](const Request& req, Response& res) {
|
||||
svr.stop();
|
||||
});
|
||||
|
||||
svr.listen("localhost", 1234);
|
||||
}
|
||||
```
|
||||
|
||||
`Post`, `Put`, `Delete` and `Options` methods are also supported.
|
||||
|
||||
### Bind a socket to multiple interfaces and any available port
|
||||
|
||||
```cpp
|
||||
int port = svr.bind_to_any_port("0.0.0.0");
|
||||
svr.listen_after_bind();
|
||||
```
|
||||
|
||||
### Static File Server
|
||||
|
||||
```cpp
|
||||
// Mount / to ./www directory
|
||||
auto ret = svr.set_mount_point("/", "./www");
|
||||
if (!ret) {
|
||||
// The specified base directory doesn't exist...
|
||||
}
|
||||
|
||||
// Mount /public to ./www directory
|
||||
ret = svr.set_mount_point("/public", "./www");
|
||||
|
||||
// Mount /public to ./www1 and ./www2 directories
|
||||
ret = svr.set_mount_point("/public", "./www1"); // 1st order to search
|
||||
ret = svr.set_mount_point("/public", "./www2"); // 2nd order to search
|
||||
|
||||
// Remove mount /
|
||||
ret = svr.remove_mount_point("/");
|
||||
|
||||
// Remove mount /public
|
||||
ret = svr.remove_mount_point("/public");
|
||||
```
|
||||
|
||||
```cpp
|
||||
// User defined file extension and MIME type mappings
|
||||
svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c");
|
||||
svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c");
|
||||
svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");
|
||||
```
|
||||
|
||||
The followings are built-in mappings:
|
||||
|
||||
| Extension | MIME Type | Extension | MIME Type |
|
||||
| :--------- | :-------------------------- | :--------- | :-------------------------- |
|
||||
| css | text/css | mpga | audio/mpeg |
|
||||
| csv | text/csv | weba | audio/webm |
|
||||
| txt | text/plain | wav | audio/wave |
|
||||
| vtt | text/vtt | otf | font/otf |
|
||||
| html, htm | text/html | ttf | font/ttf |
|
||||
| apng | image/apng | woff | font/woff |
|
||||
| avif | image/avif | woff2 | font/woff2 |
|
||||
| bmp | image/bmp | 7z | application/x-7z-compressed |
|
||||
| gif | image/gif | atom | application/atom+xml |
|
||||
| png | image/png | pdf | application/pdf |
|
||||
| svg | image/svg+xml | mjs, js | application/javascript |
|
||||
| webp | image/webp | json | application/json |
|
||||
| ico | image/x-icon | rss | application/rss+xml |
|
||||
| tif | image/tiff | tar | application/x-tar |
|
||||
| tiff | image/tiff | xhtml, xht | application/xhtml+xml |
|
||||
| jpeg, jpg | image/jpeg | xslt | application/xslt+xml |
|
||||
| mp4 | video/mp4 | xml | application/xml |
|
||||
| mpeg | video/mpeg | gz | application/gzip |
|
||||
| webm | video/webm | zip | application/zip |
|
||||
| mp3 | audio/mp3 | wasm | application/wasm |
|
||||
|
||||
### File request handler
|
||||
|
||||
```cpp
|
||||
// The handler is called right before the response is sent to a client
|
||||
svr.set_file_request_handler([](const Request &req, Response &res) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: These static file server methods are not thread-safe.
|
||||
|
||||
### Logging
|
||||
|
||||
```cpp
|
||||
svr.set_logger([](const auto& req, const auto& res) {
|
||||
your_logger(req, res);
|
||||
});
|
||||
```
|
||||
|
||||
### Error handler
|
||||
|
||||
```cpp
|
||||
svr.set_error_handler([](const auto& req, auto& res) {
|
||||
auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
|
||||
char buf[BUFSIZ];
|
||||
snprintf(buf, sizeof(buf), fmt, res.status);
|
||||
res.set_content(buf, "text/html");
|
||||
});
|
||||
```
|
||||
|
||||
### Exception handler
|
||||
The exception handler gets called if a user routing handler throws an error.
|
||||
|
||||
```cpp
|
||||
svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) {
|
||||
auto fmt = "<h1>Error 500</h1><p>%s</p>";
|
||||
char buf[BUFSIZ];
|
||||
try {
|
||||
std::rethrow_exception(ep);
|
||||
} catch (std::exception &e) {
|
||||
snprintf(buf, sizeof(buf), fmt, e.what());
|
||||
} catch (...) { // See the following NOTE
|
||||
snprintf(buf, sizeof(buf), fmt, "Unknown Exception");
|
||||
}
|
||||
res.set_content(buf, "text/html");
|
||||
res.status = 500;
|
||||
});
|
||||
```
|
||||
|
||||
NOTE: if you don't provide the `catch (...)` block for a rethrown exception pointer, an uncaught exception will end up causing the server crash. Be careful!
|
||||
|
||||
### Pre routing handler
|
||||
|
||||
```cpp
|
||||
svr.set_pre_routing_handler([](const auto& req, auto& res) {
|
||||
if (req.path == "/hello") {
|
||||
res.set_content("world", "text/html");
|
||||
return Server::HandlerResponse::Handled;
|
||||
}
|
||||
return Server::HandlerResponse::Unhandled;
|
||||
});
|
||||
```
|
||||
|
||||
### Post routing handler
|
||||
|
||||
```cpp
|
||||
svr.set_post_routing_handler([](const auto& req, auto& res) {
|
||||
res.set_header("ADDITIONAL_HEADER", "value");
|
||||
});
|
||||
```
|
||||
|
||||
### 'multipart/form-data' POST data
|
||||
|
||||
```cpp
|
||||
svr.Post("/multipart", [&](const auto& req, auto& res) {
|
||||
auto size = req.files.size();
|
||||
auto ret = req.has_file("name1");
|
||||
const auto& file = req.get_file_value("name1");
|
||||
// file.filename;
|
||||
// file.content_type;
|
||||
// file.content;
|
||||
});
|
||||
```
|
||||
|
||||
### Receive content with a content receiver
|
||||
|
||||
```cpp
|
||||
svr.Post("/content_receiver",
|
||||
[&](const Request &req, Response &res, const ContentReader &content_reader) {
|
||||
if (req.is_multipart_form_data()) {
|
||||
// NOTE: `content_reader` is blocking until every form data field is read
|
||||
MultipartFormDataItems files;
|
||||
content_reader(
|
||||
[&](const MultipartFormData &file) {
|
||||
files.push_back(file);
|
||||
return true;
|
||||
},
|
||||
[&](const char *data, size_t data_length) {
|
||||
files.back().content.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
std::string body;
|
||||
content_reader([&](const char *data, size_t data_length) {
|
||||
body.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Send content with the content provider
|
||||
|
||||
```cpp
|
||||
const size_t DATA_CHUNK_SIZE = 4;
|
||||
|
||||
svr.Get("/stream", [&](const Request &req, Response &res) {
|
||||
auto data = new std::string("abcdefg");
|
||||
|
||||
res.set_content_provider(
|
||||
data->size(), // Content length
|
||||
"text/plain", // Content type
|
||||
[data](size_t offset, size_t length, DataSink &sink) {
|
||||
const auto &d = *data;
|
||||
sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE));
|
||||
return true; // return 'false' if you want to cancel the process.
|
||||
},
|
||||
[data](bool success) { delete data; });
|
||||
});
|
||||
```
|
||||
|
||||
Without content length:
|
||||
|
||||
```cpp
|
||||
svr.Get("/stream", [&](const Request &req, Response &res) {
|
||||
res.set_content_provider(
|
||||
"text/plain", // Content type
|
||||
[&](size_t offset, DataSink &sink) {
|
||||
if (/* there is still data */) {
|
||||
std::vector<char> data;
|
||||
// prepare data...
|
||||
sink.write(data.data(), data.size());
|
||||
} else {
|
||||
sink.done(); // No more data
|
||||
}
|
||||
return true; // return 'false' if you want to cancel the process.
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Chunked transfer encoding
|
||||
|
||||
```cpp
|
||||
svr.Get("/chunked", [&](const Request& req, Response& res) {
|
||||
res.set_chunked_content_provider(
|
||||
"text/plain",
|
||||
[](size_t offset, DataSink &sink) {
|
||||
sink.write("123", 3);
|
||||
sink.write("345", 3);
|
||||
sink.write("789", 3);
|
||||
sink.done(); // No more data
|
||||
return true; // return 'false' if you want to cancel the process.
|
||||
}
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
With trailer:
|
||||
|
||||
```cpp
|
||||
svr.Get("/chunked", [&](const Request& req, Response& res) {
|
||||
res.set_header("Trailer", "Dummy1, Dummy2");
|
||||
res.set_chunked_content_provider(
|
||||
"text/plain",
|
||||
[](size_t offset, DataSink &sink) {
|
||||
sink.write("123", 3);
|
||||
sink.write("345", 3);
|
||||
sink.write("789", 3);
|
||||
sink.done_with_trailer({
|
||||
{"Dummy1", "DummyVal1"},
|
||||
{"Dummy2", "DummyVal2"}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 'Expect: 100-continue' handler
|
||||
|
||||
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
|
||||
|
||||
```cpp
|
||||
// Send a '417 Expectation Failed' response.
|
||||
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
|
||||
return 417;
|
||||
});
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Send a final status without reading the message body.
|
||||
svr.set_expect_100_continue_handler([](const Request &req, Response &res) {
|
||||
return res.status = 401;
|
||||
});
|
||||
```
|
||||
|
||||
### Keep-Alive connection
|
||||
|
||||
```cpp
|
||||
svr.set_keep_alive_max_count(2); // Default is 5
|
||||
svr.set_keep_alive_timeout(10); // Default is 5
|
||||
```
|
||||
|
||||
### Timeout
|
||||
|
||||
```c++
|
||||
svr.set_read_timeout(5, 0); // 5 seconds
|
||||
svr.set_write_timeout(5, 0); // 5 seconds
|
||||
svr.set_idle_interval(0, 100000); // 100 milliseconds
|
||||
```
|
||||
|
||||
### Set maximum payload length for reading a request body
|
||||
|
||||
```c++
|
||||
svr.set_payload_max_length(1024 * 1024 * 512); // 512MB
|
||||
```
|
||||
|
||||
### Server-Sent Events
|
||||
|
||||
Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/example/ssesvr.cc) and [Client example](https://github.com/yhirose/cpp-httplib/blob/master/example/ssecli.cc).
|
||||
|
||||
### Default thread pool support
|
||||
|
||||
`ThreadPool` is used as a **default** task queue, and the default thread count is 8, or `std::thread::hardware_concurrency()`. You can change it with `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
|
||||
If you want to set the thread count at runtime, there is no convenient way... But here is how.
|
||||
|
||||
```cpp
|
||||
svr.new_task_queue = [] { return new ThreadPool(12); };
|
||||
```
|
||||
|
||||
### Override the default thread pool with yours
|
||||
|
||||
You can supply your own thread pool implementation according to your need.
|
||||
|
||||
```cpp
|
||||
class YourThreadPoolTaskQueue : public TaskQueue {
|
||||
public:
|
||||
YourThreadPoolTaskQueue(size_t n) {
|
||||
pool_.start_with_thread_count(n);
|
||||
}
|
||||
|
||||
virtual void enqueue(std::function<void()> fn) override {
|
||||
pool_.enqueue(fn);
|
||||
}
|
||||
|
||||
virtual void shutdown() override {
|
||||
pool_.shutdown_gracefully();
|
||||
}
|
||||
|
||||
private:
|
||||
YourThreadPool pool_;
|
||||
};
|
||||
|
||||
svr.new_task_queue = [] {
|
||||
return new YourThreadPoolTaskQueue(12);
|
||||
};
|
||||
```
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
```c++
|
||||
#include <httplib.h>
|
||||
#include <iostream>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
httplib::Client cli("localhost", 1234);
|
||||
|
||||
if (auto res = cli.Get("/hi")) {
|
||||
if (res->status == 200) {
|
||||
std::cout << res->body << std::endl;
|
||||
}
|
||||
} else {
|
||||
auto err = res.error();
|
||||
std::cout << "HTTP error: " << httplib::to_string(err) << std::endl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: Constructor with scheme-host-port string is now supported!
|
||||
|
||||
```c++
|
||||
httplib::Client cli("localhost");
|
||||
httplib::Client cli("localhost:8080");
|
||||
httplib::Client cli("http://localhost");
|
||||
httplib::Client cli("http://localhost:8080");
|
||||
httplib::Client cli("https://localhost");
|
||||
httplib::SSLClient cli("localhost");
|
||||
```
|
||||
|
||||
### Error code
|
||||
|
||||
Here is the list of errors from `Result::error()`.
|
||||
|
||||
```c++
|
||||
enum Error {
|
||||
Success = 0,
|
||||
Unknown,
|
||||
Connection,
|
||||
BindIPAddress,
|
||||
Read,
|
||||
Write,
|
||||
ExceedRedirectCount,
|
||||
Canceled,
|
||||
SSLConnection,
|
||||
SSLLoadingCerts,
|
||||
SSLServerVerification,
|
||||
UnsupportedMultipartBoundaryChars,
|
||||
Compression,
|
||||
ConnectionTimeout,
|
||||
};
|
||||
```
|
||||
|
||||
### GET with HTTP headers
|
||||
|
||||
```c++
|
||||
httplib::Headers headers = {
|
||||
{ "Accept-Encoding", "gzip, deflate" }
|
||||
};
|
||||
auto res = cli.Get("/hi", headers);
|
||||
```
|
||||
or
|
||||
```c++
|
||||
auto res = cli.Get("/hi", {{"Accept-Encoding", "gzip, deflate"}});
|
||||
```
|
||||
or
|
||||
```c++
|
||||
cli.set_default_headers({
|
||||
{ "Accept-Encoding", "gzip, deflate" }
|
||||
});
|
||||
auto res = cli.Get("/hi");
|
||||
```
|
||||
|
||||
### POST
|
||||
|
||||
```c++
|
||||
res = cli.Post("/post", "text", "text/plain");
|
||||
res = cli.Post("/person", "name=john1¬e=coder", "application/x-www-form-urlencoded");
|
||||
```
|
||||
|
||||
### POST with parameters
|
||||
|
||||
```c++
|
||||
httplib::Params params;
|
||||
params.emplace("name", "john");
|
||||
params.emplace("note", "coder");
|
||||
|
||||
auto res = cli.Post("/post", params);
|
||||
```
|
||||
or
|
||||
|
||||
```c++
|
||||
httplib::Params params{
|
||||
{ "name", "john" },
|
||||
{ "note", "coder" }
|
||||
};
|
||||
|
||||
auto res = cli.Post("/post", params);
|
||||
```
|
||||
|
||||
### POST with Multipart Form Data
|
||||
|
||||
```c++
|
||||
httplib::MultipartFormDataItems items = {
|
||||
{ "text1", "text default", "", "" },
|
||||
{ "text2", "aωb", "", "" },
|
||||
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
|
||||
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
|
||||
{ "file3", "", "", "application/octet-stream" },
|
||||
};
|
||||
|
||||
auto res = cli.Post("/multipart", items);
|
||||
```
|
||||
|
||||
### PUT
|
||||
|
||||
```c++
|
||||
res = cli.Put("/resource/foo", "text", "text/plain");
|
||||
```
|
||||
|
||||
### DELETE
|
||||
|
||||
```c++
|
||||
res = cli.Delete("/resource/foo");
|
||||
```
|
||||
|
||||
### OPTIONS
|
||||
|
||||
```c++
|
||||
res = cli.Options("*");
|
||||
res = cli.Options("/resource/foo");
|
||||
```
|
||||
|
||||
### Timeout
|
||||
|
||||
```c++
|
||||
cli.set_connection_timeout(0, 300000); // 300 milliseconds
|
||||
cli.set_read_timeout(5, 0); // 5 seconds
|
||||
cli.set_write_timeout(5, 0); // 5 seconds
|
||||
```
|
||||
|
||||
### Receive content with a content receiver
|
||||
|
||||
```c++
|
||||
std::string body;
|
||||
|
||||
auto res = cli.Get("/large-data",
|
||||
[&](const char *data, size_t data_length) {
|
||||
body.append(data, data_length);
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
```cpp
|
||||
std::string body;
|
||||
|
||||
auto res = cli.Get(
|
||||
"/stream", Headers(),
|
||||
[&](const Response &response) {
|
||||
EXPECT_EQ(200, response.status);
|
||||
return true; // return 'false' if you want to cancel the request.
|
||||
},
|
||||
[&](const char *data, size_t data_length) {
|
||||
body.append(data, data_length);
|
||||
return true; // return 'false' if you want to cancel the request.
|
||||
});
|
||||
```
|
||||
|
||||
### Send content with a content provider
|
||||
|
||||
```cpp
|
||||
std::string body = ...;
|
||||
|
||||
auto res = cli.Post(
|
||||
"/stream", body.size(),
|
||||
[](size_t offset, size_t length, DataSink &sink) {
|
||||
sink.write(body.data() + offset, length);
|
||||
return true; // return 'false' if you want to cancel the request.
|
||||
},
|
||||
"text/plain");
|
||||
```
|
||||
|
||||
### Chunked transfer encoding
|
||||
|
||||
```cpp
|
||||
auto res = cli.Post(
|
||||
"/stream",
|
||||
[](size_t offset, DataSink &sink) {
|
||||
sink.os << "chunked data 1";
|
||||
sink.os << "chunked data 2";
|
||||
sink.os << "chunked data 3";
|
||||
sink.done();
|
||||
return true; // return 'false' if you want to cancel the request.
|
||||
},
|
||||
"text/plain");
|
||||
```
|
||||
|
||||
### With Progress Callback
|
||||
|
||||
```cpp
|
||||
httplib::Client client(url, port);
|
||||
|
||||
// prints: 0 / 000 bytes => 50% complete
|
||||
auto res = cli.Get("/", [](uint64_t len, uint64_t total) {
|
||||
printf("%lld / %lld bytes => %d%% complete\n",
|
||||
len, total,
|
||||
(int)(len*100/total));
|
||||
return true; // return 'false' if you want to cancel the request.
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Authentication
|
||||
|
||||
```cpp
|
||||
// Basic Authentication
|
||||
cli.set_basic_auth("user", "pass");
|
||||
|
||||
// Digest Authentication
|
||||
cli.set_digest_auth("user", "pass");
|
||||
|
||||
// Bearer Token Authentication
|
||||
cli.set_bearer_token_auth("token");
|
||||
```
|
||||
|
||||
NOTE: OpenSSL is required for Digest Authentication.
|
||||
|
||||
### Proxy server support
|
||||
|
||||
```cpp
|
||||
cli.set_proxy("host", port);
|
||||
|
||||
// Basic Authentication
|
||||
cli.set_proxy_basic_auth("user", "pass");
|
||||
|
||||
// Digest Authentication
|
||||
cli.set_proxy_digest_auth("user", "pass");
|
||||
|
||||
// Bearer Token Authentication
|
||||
cli.set_proxy_bearer_token_auth("pass");
|
||||
```
|
||||
|
||||
NOTE: OpenSSL is required for Digest Authentication.
|
||||
|
||||
### Range
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("httpbin.org");
|
||||
|
||||
auto res = cli.Get("/range/32", {
|
||||
httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10'
|
||||
});
|
||||
// res->status should be 206.
|
||||
// res->body should be "bcdefghijk".
|
||||
```
|
||||
|
||||
```cpp
|
||||
httplib::make_range_header({{1, 10}, {20, -1}}) // 'Range: bytes=1-10, 20-'
|
||||
httplib::make_range_header({{100, 199}, {500, 599}}) // 'Range: bytes=100-199, 500-599'
|
||||
httplib::make_range_header({{0, 0}, {-1, 1}}) // 'Range: bytes=0-0, -1'
|
||||
```
|
||||
|
||||
### Keep-Alive connection
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("localhost", 1234);
|
||||
|
||||
cli.Get("/hello"); // with "Connection: close"
|
||||
|
||||
cli.set_keep_alive(true);
|
||||
cli.Get("/world");
|
||||
|
||||
cli.set_keep_alive(false);
|
||||
cli.Get("/last-request"); // with "Connection: close"
|
||||
```
|
||||
|
||||
### Redirect
|
||||
|
||||
```cpp
|
||||
httplib::Client cli("yahoo.com");
|
||||
|
||||
auto res = cli.Get("/");
|
||||
res->status; // 301
|
||||
|
||||
cli.set_follow_location(true);
|
||||
res = cli.Get("/");
|
||||
res->status; // 200
|
||||
```
|
||||
|
||||
### Use a specific network interface
|
||||
|
||||
NOTE: This feature is not available on Windows, yet.
|
||||
|
||||
```cpp
|
||||
cli.set_interface("eth0"); // Interface name, IP address or host name
|
||||
```
|
||||
|
||||
Compression
|
||||
-----------
|
||||
|
||||
The server can apply compression to the following MIME type contents:
|
||||
|
||||
* all text types except text/event-stream
|
||||
* image/svg+xml
|
||||
* application/javascript
|
||||
* application/json
|
||||
* application/xml
|
||||
* application/xhtml+xml
|
||||
|
||||
### Zlib Support
|
||||
|
||||
'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. `libz` should be linked.
|
||||
|
||||
### Brotli Support
|
||||
|
||||
Brotli compression is available with `CPPHTTPLIB_BROTLI_SUPPORT`. Necessary libraries should be linked.
|
||||
Please see https://github.com/google/brotli for more detail.
|
||||
|
||||
### Compress request body on client
|
||||
|
||||
```c++
|
||||
cli.set_compress(true);
|
||||
res = cli.Post("/resource/foo", "...", "text/plain");
|
||||
```
|
||||
|
||||
### Compress response body on client
|
||||
|
||||
```c++
|
||||
cli.set_decompress(false);
|
||||
res = cli.Get("/resource/foo", {{"Accept-Encoding", "gzip, deflate, br"}});
|
||||
res->body; // Compressed data
|
||||
```
|
||||
|
||||
Use `poll` instead of `select`
|
||||
------------------------------
|
||||
|
||||
`select` system call is used as default since it's more widely supported. If you want to let cpp-httplib use `poll` instead, you can do so with `CPPHTTPLIB_USE_POLL`.
|
||||
|
||||
|
||||
Split httplib.h into .h and .cc
|
||||
-------------------------------
|
||||
|
||||
```console
|
||||
$ ./split.py -h
|
||||
usage: split.py [-h] [-e EXTENSION] [-o OUT]
|
||||
|
||||
This script splits httplib.h into .h and .cc parts.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-e EXTENSION, --extension EXTENSION
|
||||
extension of the implementation file (default: cc)
|
||||
-o OUT, --out OUT where to write the files (default: out)
|
||||
|
||||
$ ./split.py
|
||||
Wrote out/httplib.h and out/httplib.cc
|
||||
```
|
||||
|
||||
NOTE
|
||||
----
|
||||
|
||||
### g++
|
||||
|
||||
g++ 4.8 and below cannot build this library since `<regex>` in the versions are [broken](https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions).
|
||||
|
||||
### Windows
|
||||
|
||||
Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32_LEAN_AND_MEAN` beforehand.
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
#include <Windows.h>
|
||||
```
|
||||
|
||||
```cpp
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <httplib.h>
|
||||
```
|
||||
|
||||
NOTE: cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance.
|
||||
|
||||
NOTE: Windows 8 or lower, Visual Studio 2013 or lower, and Cygwin on Windows are not supported.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT license (© 2023 Yuji Hirose)
|
||||
|
||||
Special Thanks To
|
||||
-----------------
|
||||
|
||||
[These folks](https://github.com/yhirose/cpp-httplib/graphs/contributors) made great contributions to polish this library to totally another level from a simple toy!
|
||||
8794
include/httplib.h
Normal file
8794
include/httplib.h
Normal file
File diff suppressed because it is too large
Load Diff
563
src/NetRequest.cpp
Normal file
563
src/NetRequest.cpp
Normal 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 ¶ms)
|
||||
{
|
||||
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 ¶ms, 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;
|
||||
}
|
||||
}
|
||||
313
src/Netra.cpp
313
src/Netra.cpp
@@ -156,26 +156,19 @@ namespace QCL
|
||||
/**
|
||||
* @brief 单次接收指定客户端数据
|
||||
* @param clientSock 客户端socket
|
||||
*
|
||||
* 注意:此函数在当前设计中未在线程中使用,仅演示用。
|
||||
*/
|
||||
char *TcpServer::receiveFromClient(int clientSock, bool flag)
|
||||
std::string TcpServer::receiveFromClient(int clientSock, bool flag)
|
||||
{
|
||||
char buffer[1024];
|
||||
std::memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
ssize_t bytesReceived = 0;
|
||||
|
||||
if (flag)
|
||||
bytesReceived = recv(clientSock, buffer, sizeof(buffer) - 1, 0);
|
||||
else
|
||||
bytesReceived = recv(clientSock, buffer, sizeof(buffer) - 1, MSG_DONTWAIT);
|
||||
int flags = flag ? 0 : MSG_DONTWAIT;
|
||||
ssize_t bytesReceived = recv(clientSock, buffer, sizeof(buffer) - 1, flags);
|
||||
|
||||
if (bytesReceived <= 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return strdup(buffer); // 返回动态分配的字符串副本
|
||||
return {};
|
||||
|
||||
return std::string(buffer, bytesReceived);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,30 +231,6 @@ namespace QCL
|
||||
return writeToFile(content, std::ios::out | std::ios::app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按位置写入(线程安全)
|
||||
*/
|
||||
bool WriteFile::writeOriginal(const std::string &content, std::streampos position)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
|
||||
std::ofstream file(filePath_, std::ios::in | std::ios::out);
|
||||
if (!file.is_open())
|
||||
{
|
||||
// 文件不存在则创建
|
||||
file.open(filePath_, std::ios::out);
|
||||
file.close();
|
||||
file.open(filePath_, std::ios::in | std::ios::out);
|
||||
}
|
||||
if (!file.is_open())
|
||||
return false;
|
||||
|
||||
file.seekp(position);
|
||||
file << content;
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 覆盖写二进制(线程安全)
|
||||
*/
|
||||
@@ -306,37 +275,168 @@ namespace QCL
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算第一个指定字节序列前的字节数(包含该字节序列本身)
|
||||
*/
|
||||
long WriteFile::countBytesBeforePattern(const std::string &pattern)
|
||||
size_t WriteFile::countBytesPattern(const std::string &pattern, bool includePattern)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
|
||||
if (pattern.empty())
|
||||
return -1;
|
||||
return 0;
|
||||
|
||||
std::ifstream file(filePath_, std::ios::binary); // 二进制模式防止编码干扰
|
||||
std::ifstream file(filePath_, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
return -1;
|
||||
return 0;
|
||||
|
||||
// 将整个文件读入内存
|
||||
std::vector<char> buffer((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
const size_t chunkSize = 4096;
|
||||
std::string buffer;
|
||||
buffer.reserve(chunkSize * 2);
|
||||
|
||||
// 在 buffer 中查找 pattern
|
||||
auto it = std::search(buffer.begin(), buffer.end(),
|
||||
pattern.begin(), pattern.end());
|
||||
size_t totalRead = 0;
|
||||
char chunk[chunkSize];
|
||||
|
||||
if (it == buffer.end())
|
||||
while (file.read(chunk, chunkSize) || file.gcount() > 0)
|
||||
{
|
||||
return -1; // 没找到
|
||||
size_t bytesRead = file.gcount();
|
||||
buffer.append(chunk, bytesRead);
|
||||
|
||||
size_t pos = buffer.find(pattern);
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
size_t absolutePos = totalRead + pos; // 关键:加上 totalRead
|
||||
return includePattern ? (absolutePos + pattern.size()) : absolutePos;
|
||||
}
|
||||
|
||||
if (buffer.size() > pattern.size())
|
||||
buffer.erase(0, buffer.size() - pattern.size());
|
||||
|
||||
totalRead += bytesRead; // 读完后再累计
|
||||
}
|
||||
|
||||
// 计算从开头到 pattern 结束的字节数
|
||||
size_t pos = std::distance(buffer.begin(), it);
|
||||
return static_cast<long>(pos + pattern.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool WriteFile::writeAfterPatternOrAppend(const std::string &pattern, const std::string &content)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
|
||||
// 读取整个文件
|
||||
std::ifstream in(filePath_, std::ios::binary);
|
||||
if (!in.is_open())
|
||||
return false;
|
||||
|
||||
std::string fileData((std::istreambuf_iterator<char>(in)), {});
|
||||
in.close();
|
||||
|
||||
size_t pos = fileData.find(pattern);
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
// 模式存在,插入位置在模式结尾
|
||||
pos += pattern.size();
|
||||
|
||||
// 删除模式后所有内容
|
||||
if (pos < fileData.size())
|
||||
fileData.erase(pos);
|
||||
|
||||
// 插入新内容
|
||||
fileData.insert(pos, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 模式不存在,直接追加到文件末尾
|
||||
if (!fileData.empty() && fileData.back() != '\n')
|
||||
fileData += '\n'; // 保证换行
|
||||
fileData += content;
|
||||
}
|
||||
|
||||
// 写回文件
|
||||
std::ofstream out(filePath_, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open())
|
||||
return false;
|
||||
|
||||
out.write(fileData.data(), fileData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteFile::overwriteAtPos(const std::string &content, size_t pos, size_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
|
||||
// 打开文件读取
|
||||
std::ifstream in(filePath_, std::ios::binary);
|
||||
if (!in.is_open())
|
||||
return false;
|
||||
|
||||
std::string fileData((std::istreambuf_iterator<char>(in)), {});
|
||||
in.close();
|
||||
|
||||
// 边界检查
|
||||
if (pos >= fileData.size())
|
||||
return false; // pos 超过文件范围,无法覆盖
|
||||
|
||||
// 生成要覆盖的实际数据块
|
||||
std::string overwriteBlock;
|
||||
if (content.size() >= length)
|
||||
{
|
||||
overwriteBlock = content.substr(0, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
overwriteBlock = content;
|
||||
overwriteBlock.append(length - content.size(), '\0'); // 补齐
|
||||
}
|
||||
|
||||
// 计算实际可写范围
|
||||
size_t maxWritable = std::min(length, fileData.size() - pos);
|
||||
|
||||
// 覆盖
|
||||
fileData.replace(pos, maxWritable, overwriteBlock.substr(0, maxWritable));
|
||||
|
||||
// 写回文件
|
||||
std::ofstream out(filePath_, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open())
|
||||
return false;
|
||||
|
||||
out.write(fileData.data(), fileData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteFile::insertAfterPos(const std::string &content, size_t pos, size_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex_);
|
||||
|
||||
// 打开文件读取
|
||||
std::ifstream in(filePath_, std::ios::binary);
|
||||
if (!in.is_open())
|
||||
return false;
|
||||
|
||||
std::string fileData((std::istreambuf_iterator<char>(in)), {});
|
||||
in.close();
|
||||
|
||||
// 边界检查
|
||||
if (pos > fileData.size())
|
||||
pos = fileData.size(); // 如果 pos 超出范围,就视为文件末尾
|
||||
|
||||
// 生成要插入的实际数据块
|
||||
std::string insertBlock;
|
||||
if (content.size() >= length)
|
||||
{
|
||||
insertBlock = content.substr(0, length); // 只取前 length 个字节
|
||||
}
|
||||
else
|
||||
{
|
||||
insertBlock = content; // 全部内容
|
||||
insertBlock.append(length - content.size(), '\0'); // 补足空字节
|
||||
}
|
||||
|
||||
// 插入到 pos 后面
|
||||
fileData.insert(pos + 1, insertBlock);
|
||||
|
||||
// 写回文件
|
||||
std::ofstream out(filePath_, std::ios::binary | std::ios::trunc);
|
||||
if (!out.is_open())
|
||||
return false;
|
||||
|
||||
out.write(fileData.data(), fileData.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -419,32 +519,82 @@ namespace QCL
|
||||
return buffer;
|
||||
}
|
||||
|
||||
size_t ReadFile::GetBytesBefore(const std::string &marker)
|
||||
size_t ReadFile::GetBytesBefore(const std::string &marker, bool includeMarker)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
|
||||
if (!file_.is_open() && !Open())
|
||||
return 0;
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << file_.rdbuf();
|
||||
std::string content = ss.str();
|
||||
file_.clear(); // 清除EOF和错误状态
|
||||
file_.seekg(0, std::ios::beg); // 回到文件开头
|
||||
|
||||
size_t pos = content.find(marker);
|
||||
if (pos != std::string::npos)
|
||||
return pos + marker.size();
|
||||
else
|
||||
return content.size();
|
||||
const size_t chunkSize = 4096;
|
||||
std::string buffer;
|
||||
buffer.reserve(chunkSize * 2);
|
||||
|
||||
size_t totalRead = 0;
|
||||
char chunk[chunkSize];
|
||||
|
||||
while (file_.read(chunk, chunkSize) || file_.gcount() > 0)
|
||||
{
|
||||
buffer.append(chunk, file_.gcount());
|
||||
size_t pos = buffer.find(marker);
|
||||
if (pos != std::string::npos)
|
||||
{
|
||||
// 如果 includeMarker 为 true,返回包含 marker 的长度
|
||||
if (includeMarker)
|
||||
return pos + marker.size();
|
||||
else
|
||||
return pos;
|
||||
}
|
||||
|
||||
// 保留末尾部分,避免 buffer 无限增长
|
||||
if (buffer.size() > marker.size())
|
||||
buffer.erase(0, buffer.size() - marker.size());
|
||||
|
||||
totalRead += file_.gcount();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<char> ReadFile::ReadBytesFrom(size_t pos, size_t count)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
|
||||
if (!file_.is_open() && !Open())
|
||||
return {};
|
||||
|
||||
size_t filesize = GetFileSize();
|
||||
if (pos >= filesize)
|
||||
return {}; // 起始位置超出文件大小
|
||||
|
||||
file_.clear(); // 清除 EOF 和错误状态
|
||||
file_.seekg(pos, std::ios::beg);
|
||||
if (!file_)
|
||||
return {};
|
||||
|
||||
size_t bytes_to_read = count;
|
||||
if (count == 0 || pos + count > filesize)
|
||||
bytes_to_read = filesize - pos; // 读取到文件末尾
|
||||
|
||||
std::vector<char> buffer(bytes_to_read);
|
||||
file_.read(buffer.data(), bytes_to_read);
|
||||
|
||||
// 实际读取的字节数可能少于请求的数量
|
||||
buffer.resize(file_.gcount());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool ReadFile::FileExists() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
return std::filesystem::exists(filename_);
|
||||
}
|
||||
|
||||
size_t ReadFile::GetFileSize() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (!FileExists())
|
||||
return 0;
|
||||
return std::filesystem::file_size(filename_);
|
||||
@@ -468,5 +618,34 @@ namespace QCL
|
||||
for (int ii = 1; ii <= 64; ii++)
|
||||
signal(ii, SIG_IGN);
|
||||
}
|
||||
|
||||
std::string Ltrim(const std::string &s)
|
||||
{
|
||||
size_t start = 0;
|
||||
while (start < s.size() && std::isspace(static_cast<unsigned char>(s[start])))
|
||||
{
|
||||
++start;
|
||||
}
|
||||
return s.substr(start);
|
||||
}
|
||||
|
||||
std::string Rtrim(const std::string &s)
|
||||
{
|
||||
if (s.empty())
|
||||
return s;
|
||||
|
||||
size_t end = s.size();
|
||||
while (end > 0 && std::isspace(static_cast<unsigned char>(s[end - 1])))
|
||||
{
|
||||
--end;
|
||||
}
|
||||
return s.substr(0, end);
|
||||
}
|
||||
|
||||
std::string LRtrim(const std::string &s)
|
||||
{
|
||||
return Ltrim(Rtrim(s));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
Reference in New Issue
Block a user