HXLibs 是一个现代 C++ 库. 目前集合了:
-
基于
io_uring/IOCP协程 的 Http (s) / WebSocket 客户端与服务端, 服务器支持分块编码/断点续传/超时机制! 客户端支持Socks5/Http代理.本地测试 500MB 文件传输吞吐量高达 20 GB/s; wrk 压测 Http 请求并发高达 200w+ Requests/sec
-
WebSocket 服务端接口学习了 FastApi 思想, 使用简单.
-
支持聚合类反射 / 宏反射(支持别名 和 私有成员), 轻松支持 Json 的序列化与反序列化! (反序列化时对字符串的零拷贝)
还包含:
- 协程库
- 网络库
- 反射库
- Json序列化库 (基于反射)
- ORM 数据库操作 (目前仅实现了 sqlite3)
- 日志库
更多介绍
@todo...
- Linux 5.1+ || Windows
- GCC / Clang || MSVC 编译器
- C++20
CMake以子项目的方式导入使用:
# 配置是否开启 HTTPS
set(HXLIBS_ENABLE_SSL ON)
# 导入 libs 文件夹下的 HXLibs
add_subdirectory(libs/HXLibs)
target_link_libraries(YouProject PRIVATE HXLibs)Tip
新增 宏 API, 快速定义控制器, 支持依赖注入~
#include <HXLibs/net/ApiMacro.hpp> // 导入宏 & HX::net
struct LoliDAO {
uint64_t select(uint64_t id) {
return id;
}
};
HX_CONTROLLER(LoliController) {
HX_ENDPOINT_MAIN(std::shared_ptr<LoliDAO> loliDAO) {
using namespace HX::net;
addEndpoint<GET>("/", [=] ENDPOINT {
auto id = loliDAO->select(114514);
co_await res.setStatusAndContent(Status::CODE_200, std::to_string(id))
.sendRes();
})
.addEndpoint<POST>("/post", [=] ENDPOINT {
auto id = loliDAO->select(2233);
co_await res.setStatusAndContent(Status::CODE_200, std::to_string(id))
.sendRes();
});
}
};
#include <HXLibs/net/UnApiMacro.hpp> // undef 宏
int main() {
using namespace HX::net;
HttpServer server{28205};
// 依赖注入
server.addController<LoliController>(std::make_shared<LoliDAO>());
server.syncRun(1);
}下面是一个简单的服务端示例: (examples/HttpServer/01_http_server.cpp)
#include <HXLibs/net/ApiMacro.hpp> // 包含了 端点宏 ENDPOINT
#include <HXLibs/log/Log.hpp>
using namespace HX;
using namespace net;
int main() {
HttpServer ser{28205}; // 定义服务器
ser.addEndpoint<GET>("/user/{id}", [] ENDPOINT {
co_await res.setStatusAndContent(
Status::CODE_200,
"<h1>Hello"
+ req.getPathParam(0) // 获取 {id} 路径参数, 可以 req.getPathParam(0).to<uint64_t>() 快速转为数字/枚举哦~
+ "</h1>"
+ utils::DateTimeFormat::formatWithMilli()
).sendRes();
co_return;
})
.addEndpoint<GET>("/stop", [&] ENDPOINT {
// 支持手动关闭服务器
co_await res.setStatusAndContent(
Status::CODE_200, "<h1>stop server!</h1>" + utils::DateTimeFormat::formatWithMilli())
.sendRes();
req.addHeaders("Connection", "close");
ser.asyncStop();
co_return;
})
.addEndpoint<GET>("/favicon.ico", [] ENDPOINT {
log::hxLog.debug("get .ico");
co_return co_await res.useRangeTransferFile( // 传输单个文件
req.getRangeRequestView(),
"static/favicon.ico"
);
})
.addEndpoint<GET, HEAD>("/files/**", [] ENDPOINT {
using namespace std::string_literals;
using namespace std::string_view_literals;
auto path = req.getUniversalWildcardPath();
log::hxLog.debug("请求:", path, "| 断点续传:", req.getReqType() == "HEAD"sv || req.getHeaders().contains("range"));
try {
// 使用断点续传传输文件 (匹配到 `static/bigFile/**` 路径)
co_await res.useRangeTransferFile(
req.getRangeRequestView(),
"static/bigFile/"s + std::string{path.data(), path.size()}
);
log::hxLog.debug("发送完毕!");
} catch (std::exception const& ec) {
// 协程支持异常
log::hxLog.error(__FILE__, __LINE__, ":", ec.what());
}
co_return ;
});
using namespace utils;
ser.syncRun(16, 30_s); // 启动服务器, 并且阻塞
// 其中第一个参数是线程个数, 每个线程独立维护一个 事件循环
// 一个事件循环控制 该线程的所有协程
// 第二个参数是超时时间, 超时会自动断开连接 (从离开端点函数时候开始计算)
// 注意: 其时间类型定义在 include/HXLibs/utils/TimeNTTP.hpp 中, 并非标准库的时间
}其中, ENDPOINT 只是一种简写, 其被定义为:
/**
* @brief 定义标准的端点, 请求使用`req`变量, 响应使用`res`变量
*/
#define ENDPOINT ( \
[[maybe_unused]] HX::net::Request& req, \
[[maybe_unused]] HX::net::Response& res \
) -> HX::coroutine::Task<>特别的, 它还支持面向切面编程:
enum class ServerStatus : uint32_t {
Error = 0,
Ok = 1
};
struct TimeLog { // 计时器切面: 端点访问时间间隔
decltype(std::chrono::steady_clock::now()) t;
// 进入端点之前
auto before(Request&, Response&) {
t = std::chrono::steady_clock::now();
return ServerStatus::Ok; // 返回值只要可以转换为 bool 类型, 就是合法的
// 如果返回 bool{false}, 那么就会终止连接 (类似于`拦截器`)
}
// 端点结束之后
auto after(Request& req, Response&) {
auto t1 = std::chrono::steady_clock::now();
auto dt = t1 - t;
int64_t us = std::chrono::duration_cast<std::chrono::milliseconds>(dt).count();
log::hxLog.info("已响应: ", req.getPureReqPath(), "花费: ", us, " us");
return true;
}
};
HttpServer ser{28205};
ser.addEndpoint<GET>("/", [] ENDPOINT {
co_await res.setStatusAndContent(
Status::CODE_200, "<h1>这是 HXLibs::net 服务器</h1>" + utils::DateTimeFormat::formatWithMilli())
.sendRes();
co_return;
}, TimeLog{}) // <-- 面向切面编程, 可以编写任意个切面, 只需要在此传参即可下面是一个简单的客户端示例: (tests/client/01_http_client.cpp)
Tip
客户端的 http 请求都提供了协程和异步两种接口, 后者可以通过 .get() 阻塞等待获取 返回值, 以变为同步接口 (如下示例)
#include <HXLibs/net/client/HttpClient.hpp>
#include <HXLibs/log/Log.hpp>
using namespace HX;
using namespace net;
// 协程接口
coroutine::Task<> coMain() {
HttpClient cli{};
log::hxLog.debug("开始请求");
ResponseData res = co_await cli.coGet("http://httpbin.org/get");
log::hxLog.info("状态码:", res.status);
log::hxLog.info("拿到了 头:", res.headers);
log::hxLog.info("拿到了 体:", res.body);
}
int main() {
coMain().start();
// 同步接口
HttpClient cli{};
auto res = cli.get("http://httpbin.org/get").get(); // 后面的 .get() 是阻塞获取返回值
// 如果不 .get(), 那么它实际上是异步的 (内部有线程执行)
log::hxLog.info("状态码:", res.status);
log::hxLog.info("拿到了 头:", res.headers);
log::hxLog.info("拿到了 体:", res.body);
// 支持异步 API thenTry(Try<T>)
cli.get("http://httpbin.org/get").thenTry([](Try<ResponseData> resTry) -> void {
if (resTry) {
auto res = resTry.move();
log::hxLog.info("状态码:", res.status);
log::hxLog.info("拿到了 头:", res.headers);
log::hxLog.info("拿到了 体:", res.body);
} else {
log::hxLog.error("发生异常:", resTry.what());
}
return;
}).thenTry([](Try<void> /* 进一步的, 可以写为 `Try<>`, 因为默认参数: T = void*/) {
// ...
// 注意, Try<T> 的 应该是前一个的返回值类型
});
return 0;
}
void test() {
// 使用代理
HttpClient cli{HttpClientOptions{{"socks5://127.0.0.1:2334"}}};
auto res = cli.get("http://httpbin.org/get").get();
log::hxLog.info("状态码:", res.status);
log::hxLog.info("拿到了 头:", res.headers);
log::hxLog.info("拿到了 体:", res.body);
return 0;
}在 服务端, 我们提供了类似于 FastApi 的 WebSocket 接口, 使用非常方便!
详细内容请看: tests/server/02_ws_server.cpp, 里面还提供了 WebSocket 消息群发 的代码示例.
HttpServer serv{28205};
serv.addEndpoint<GET>("/ws", [] ENDPOINT {
auto ws = co_await WebSocketFactory{req, res}.accept();; // 升级为 WebSocket
struct JsonDataVo {
std::string msg;
int code;
};
JsonDataVo const vo{"Hello 客户端, 我只能通信3次!", 200};
co_await ws.sendJson(vo); // WebSocket 发送 Json 数据 (基于聚合类反射)
for (int i = 0; i < 3; ++i) {
auto res = co_await ws.recvText(); // 等待读取文本
// [!] 特别的: 如果客户端发送了 ping, 其内部会自动回应 pong
// 并且不会阻断 recvText (也就是无感知的)
// 同理, 如果客户端发送 close, 则内部也会响应 close
// 关闭 ws 后, 会抛出 {已经安全关闭连接} 的异常
// [!] 如果读取的数据格式不对, 也会抛异常: 比如期望是 Test, 但是读取到二进制
log::hxLog.info(res);
co_await ws.sendText("Hello! " + res); // 发送文本
}
co_await ws.close(); // 主动关闭 WebSocket 连接
log::hxLog.info("断开ws");
co_return ;
});在 客户端, 我们提供的是协程回调的 WebSocket, 并且同样提供了协程和异步两种接口: (tests/client/02_ws_client.cpp)
Tip
为什么是 协程回调?
实际上重点是回调, 内部会传入一个 WebSocketClient 供用户使用, 而将它作为回调是为了安全的控制 WebSocket 的生命周期. 以防止它被用户滥用.
coroutine::Task<> coMain() {
HttpClient cli{};
co_await cli.coWsLoop("ws://127.0.0.1:28205/ws",
[](WebSocketClient ws) -> coroutine::Task<> {
co_await ws.sendText("hello");
auto msg = co_await ws.recvText();
log::hxLog.info("收到: ", msg);
co_await ws.close();
}
);
}
int main() {
// 协程
coMain().start();
// 同步
HttpClient cli{};
cli.wsLoop("ws://127.0.0.1:28205/ws",
[](WebSocketClient ws) -> coroutine::Task<> {
co_await ws.sendText("hello 我是同步接口的协程回调");
auto msg = co_await ws.recvText();
log::hxLog.info("收到: ", msg);
co_await ws.close();
}
).wait();
return 0;
}T: 返回值类型P: 协程控制者 (默认的Promise<T>是懒启动, 以及co_return可以恢复到之前的协程调用者)Awaiter: 则是指定co_await的功能
其中的 runSync 方法很危险, 除非你可以保证它可以执行到 co_return 并且在此之前不会返回, 否则不要调用该方法.
template <
typename T = void,
typename P = Promise<T>,
typename Awaiter = ExitAwaiter<T, P>
>
struct [[nodiscard]] Task {
/**
* @brief 立即执行协程
* @warning 除非你可以保证它可以执行到 co_return 并且在此之前不会返回, 否则不要调用该方法
* @return constexpr auto
*/
constexpr auto runSync() const {
_handle.resume();
if constexpr (requires {
_handle.promise().result(); // Promise<T> 存在的
}) {
if (_handle.done()) [[likely]] {
return _handle.promise().result();
} else {
// 不是期望的! 协程还没有执行完毕
throw std::runtime_error{"The collaborative process has not been completed yet"};
}
}
}
};它可以让自己从当前协程中分离出来, 变成一个独立的协程 "根协程" 被调度.
// 使用示例: include/HXLibs/net/server/ConnectionHandler.hpp
coroutine::Task<> start(std::atomic_bool const& isRun) {
auto serverFd = co_await makeServerFd();
for (;;) [[likely]] {
auto fd = exception::IoUringErrorHandlingTools::check(
co_await _eventLoop.makeAioTask().prepAccept(
serverFd,
nullptr,
nullptr,
0
)
);
log::hxLog.debug("有新的连接:", fd);
// 直接从当前协程分离
ConnectionHandler::start<Timeout>(fd, isRun, _router, _eventLoop).detach();
}
}它可以同时启动所有的协程, 并且返回第一个完成的那个
// 常见使用场景: 超时
auto res = co_await whenAny(协程_01(), 定时器());
if (res.index() == 1)
log::hxLog.debug("超时:");
// 也可以使用运算符重载
auto ans = co_await (协程_01() || 定时器());它可以同时启动所有的协程, 并且等待所有执行完毕, 最终返回 std::tuple<Ts...> (其中 void 被表示为 NoVoid 类型)
auto [r1, t2] = co_await whenAll(协程_01(), 协程_02());
// 也可以使用运算符重载
auto [a, b, c] = co_await (协程_01() && 协程_02() && 协程_03());Tip
暂时不支持 混合 && 和 || 的调用: 如: co_await ((协程_01() && 协程_02()) || 协程_03())
见 EventLoop.hpp (Linux基于io_uring实现, Win基于IOCP实现)
以及协程定时器 (基于红黑树实现快速删除 (析构时候直接通过迭代器删除, 无需查找)) TimerLoop.hpp
常用API:
struct EventLoop {
// 启动一个协程, 如果内部被挂起, 稍后应该调用 run(), 以从挂起中恢复
template <CoroutineObject T>
void start(T& mainTask);
// 同步等待一个协程完成 (在此之前, 应该保证事件循环为空, 否则会抛出异常)
// 如果 协程 mainTask 抛出异常, 则 sync 会捕获并且重新抛出
template <CoroutineObject T, typename Res = AwaiterReturnValue<T>>
Res sync(T&& mainTask);
// 启动事件循环
void run();
};Tip
如果你的类有 EventLoop, 那么你可以轻易的在析构的时候通过调用_eventLoop.sync(this->close(...))来实现所谓 RAII协程
包含协程和线程的混用. 可在协程中挂起等待线程池的任务:
#include <HXLibs/coroutine/loop/EventLoop.hpp>
#include <HXLibs/coroutine/loop/ThreadLoop.hpp>
#include <HXLibs/container/ThreadPool.hpp>
#include <HXLibs/container/FutureResult.hpp>
#include <HXLibs/coroutine/task/Task.hpp>
#include <HXLibs/coroutine/awaiter/WhenAny.hpp>
#include <HXLibs/log/Log.hpp>
using namespace HX;
int main() {
using namespace std::chrono;
// 线程池
container::ThreadPool threadPool{};
threadPool.run<container::ThreadPool::Model::FixedSizeAndNoCheck>();
// 协程调度器
coroutine::EventLoop loop{};
loop.sync([&]() -> coroutine::Task<> {
log::hxLog.info("Hello Wow");
// 简单协程任务
auto twoTask = co_await ([&]() -> coroutine::Task<> {
log::hxLog.info("萌萌滴干活!");
co_await loop.makeTimer().sleepFor(3s);
}() || loop.makeTimer().sleepFor(1s));
log::hxLog.info("第", twoTask.index() + 1, "个任务完成了!");
// 简单线程池任务
auto res = co_await threadPool.addTask([]{
log::hxLog.info("sleep: 3s [begin]~");
std::this_thread::sleep_for(3s);
return 233;
}).thenTry([](container::Try<int> t) {
if (!t) {
t.rethrow();
}
return 2233 + t.move();
}).thenTry([](auto&& t) {
if (!t) {
t.rethrow();
}
return 2233 + t.move();
}).via(loop);
log::hxLog.info("sleep: 3s [end], val =", res);
}());
}#include <HXLibs/reflection/MemberName.hpp>
struct Test {
int a;
std::string name;
};
// 支持获取字段个数 [编译期]
constexpr auto Cnt = reflection::membersCount<Test>();
CHECK(Cnt == 2);
static_assert(Cnt == 2, "");
// 支持从索引访问字段名 [编译期]
constexpr auto Name = reflection::getMembersNames<Test>();
CHECK(Name[0] == "name01");
CHECK(Name[1] == "name02");
static_assert(Name[0] == "name01", "");
static_assert(Name[1] == "name02", "");
// 支持获取字段名到索引的哈希 [编译期]
constexpr auto map = reflection::getMembersNamesMap<Test>();
static_assert(map.at("name01") == 0, "");
static_assert(map.at("name02") == 1, "");
static_assert(map.find("_name") == map.end(), "");
// 获取到的哈希表是编译期常量, 可以进行编译期映射 [编译期]
[[maybe_unused]] constexpr auto _ = std::index_sequence<
map.at("name01"),
map.at("name02")
>{};
// (部分)编译期 for 循环
Test obj;
reflection::forEach(obj, [&] <std::size_t I> (
std::index_sequence<I>, // 字段索引 I (编译器字面量)
auto name, // 字段名称 std::string_view
auto& val // 字段引用 (如果传入的是 Test const&, 那么对应的 val 也是 auto const&)
) {
if constexpr (I == 0)
val = 0;
if constexpr (I == 1)
val = "index = 1";
});在 HXLibs/reflection/ReflectionMacro.hpp 中, 我们支持用户使用宏来生成反射代码, 它可以反射私有字段:
- 类内反射, 支持私有成员变量
#include <HXLibs/reflection/ReflectionMacro.hpp> // 头文件
struct HXTest {
private:
int a{};
std::string b{};
double c{};
public:
HX_REFL(a, b) // 指定 反射 a, b, 而可以不反射 c
};
#include <HXLibs/reflection/json/JsonRead.hpp>
#include <HXLibs/reflection/json/JsonWrite.hpp>
TEST_CASE("宏反射内私有成员") {
using namespace HX;
[[maybe_unused]] constexpr auto N = reflection::membersCountVal<HXTest>;
constexpr auto name = reflection::getMembersNames<HXTest>();
static_assert(name[0] == "a", ""); // 依旧是编译期反射
HXTest t{};
[[maybe_unused]] auto tr = reflection::internal::getObjTie(t);
[[maybe_unused]] auto res = HXTest::visit(t);
[[maybe_unused]] auto cnt = reflection::HasReflectionCount<HXTest const&>;
// 同样支持 forEach
reflection::forEach(t, [] <std::size_t Idx> (std::index_sequence<Idx>, auto name, auto& v) {
if constexpr (Idx == 0) {
v = 2233;
} else if constexpr (Idx == 1) {
v = "666";
}
log::hxLog.info(Idx, name, v);
});
// 只要注册了, 就可以随意使用 json / log 以反射的实现 (前提是该字段类型是支持的)
HXTest newT;
std::string s;
reflection::toJson(t, s);
reflection::fromJson(newT, s);
log::hxLog.info(newT);
}- 特别的, 支持起 别名:
struct HXTest_2 {
private:
[[maybe_unused]] int a{};
[[maybe_unused]] std::string b{};
[[maybe_unused]] double c{};
public:
// 支持起别名的反射宏
HX_REFL_AS(
// 格式: 原变量名, 别名
a, int_a,
c, double_c
) // 注意: 格式一定要统一, 必须为偶数个参数, 顺序对应~
};
TEST_CASE("宏反射内私有成员 支持别名") {
using namespace HX;
[[maybe_unused]] constexpr auto N = reflection::membersCountVal<HXTest_2>;
constexpr auto name = reflection::getMembersNames<HXTest_2>();
[[maybe_unused]] HXTest_2 t{};
// 此处是别名!
static_assert(name[0] == "int_a", "");
[[maybe_unused]] auto tr = reflection::internal::getObjTie(t);
HXTest_2 newT;
std::string s;
log::hxLog.info(t);
reflection::forEach(t, [] <std::size_t Idx> (std::index_sequence<Idx>, auto name, auto& v) {
if constexpr (Idx == 0) {
v = 666;
} else if constexpr (Idx == 1) {
v = 0.721;
}
log::hxLog.info(Idx, name, v);
});
// 依旧支持序列化和反序列化、log等等, 均以 别名 使用
reflection::toJson(t, s);
reflection::fromJson(newT, s);
log::hxLog.info(newT);
}- 类外反射, 在宏加上
_G(global), 即为声明为全局函数的反射版本, 但是仅能反射public字段. 不如类内声明
namespace UserNs { // 用户命名空间
class MyClass {
public:
int a;
std::string b;
std::vector<MyClass> c;
};
// 定义全局反射宏, 可以在用户的命名空间内定义
HX_REFL_G(MyClass, a, b, c)
}
TEST_CASE("全局注册的反射") {
static_assert(
reflection::HasInsideReflection<UserNs::MyClass> == false
&& reflection::HasOutReflection<UserNs::MyClass> == true
&& reflection::IsReflective<UserNs::MyClass>, "");
UserNs::MyClass myCLass;
// 参数个数
static_assert(reflection::membersCountVal<decltype(myCLass)> == 3, "");
// 反射名称
constexpr auto name = reflection::getMembersNames<UserNs::MyClass>();
static_assert(name[0] == "a", "");
static_assert(name[1] == "b", "");
static_assert(name[2] == "c", "");
reflection::forEach(myCLass, [] <std::size_t Idx> (std::index_sequence<Idx>, auto name, auto& v) {
log::hxLog.debug(Idx, name, "->", v);
if constexpr (Idx == 0) {
v = 1;
} else if constexpr (Idx == 1) {
v = "b";
} else if constexpr (Idx == 2) {
v = {{1, "2", {}}};
}
});
log::hxLog.debug(myCLass); // 依据支持打印等等操作
UserNs::MyClass newT;
std::string s;
reflection::toJson(myCLass, s);
reflection::fromJson(newT, s);
CHECK(myCLass.a == newT.a);
CHECK(myCLass.b == newT.b);
CHECK(myCLass.c.size() == newT.c.size());
log::hxLog.info(newT);
}- 同理, 在宏的后面加上
_AS, 即是全局函数版本的起别名反射:
namespace UserNs { // 用户命名空间
class MyClassAs {
public:
int a;
std::string b;
std::vector<MyClassAs> c;
};
// 定义全局反射宏, 可以在用户的命名空间内定义
HX_REFL_G_AS(MyClassAs,
a, new_a,
b, new_b,
c, new_c
)
}
TEST_CASE("全局注册的反射(别名)") {
static_assert(
reflection::HasInsideReflection<UserNs::MyClassAs>
!= reflection::HasOutReflection<UserNs::MyClassAs>
&& reflection::IsReflective<UserNs::MyClassAs>, "");
UserNs::MyClassAs myCLass;
// 参数个数
static_assert(reflection::membersCountVal<decltype(myCLass)> == 3, "");
// 反射名称
constexpr auto name = reflection::getMembersNames<UserNs::MyClassAs>();
static_assert(name[0] == "new_a", "");
static_assert(name[1] == "new_b", "");
static_assert(name[2] == "new_c", "");
reflection::forEach(myCLass, [] <std::size_t Idx> (std::index_sequence<Idx>, auto name, auto& v) {
log::hxLog.debug(Idx, name, "->", v);
if constexpr (Idx == 0) {
v = 1;
} else if constexpr (Idx == 1) {
v = "b";
} else if constexpr (Idx == 2) {
v = {{1, "2", {}}};
}
});
log::hxLog.debug(myCLass); // 依据支持打印等等操作
UserNs::MyClassAs newT;
std::string s;
reflection::toJson(myCLass, s);
reflection::fromJson(newT, s);
CHECK(myCLass.a == newT.a);
CHECK(myCLass.b == newT.b);
CHECK(myCLass.c.size() == newT.c.size());
log::hxLog.info(newT);
}#include <HXLibs/reflection/json/JsonRead.hpp>
#include <HXLibs/reflection/json/JsonWrite.hpp>
#include <HXLibs/log/Log.hpp>
using namespace HX;
TEST_CASE("json 序列化") {
struct Cat {
std::string name;
int num;
std::vector<Cat> sub;
};
Cat cat = {
"咪咪",
2233,
{{
"明卡",
233,
{{
"大猫",
666,
{}
}}
}, {
"喵喵",
114514,
{{}}
}}
};
std::string catStr;
reflection::toJson(cat, catStr);
log::hxLog.info("元素:", cat);
log::hxLog.info("序列化字符串:", catStr);
CHECK(catStr == R"({"name":"咪咪","num":2233,"sub":[{"name":"明卡","num":233,"sub":[{"name":"大猫","num":666,"sub":[]}]},{"name":"喵喵","num":114514,"sub":[{"name":"","num":0,"sub":[]}]}]})");
catStr.clear();
}
TEST_CASE("json 反序列化") {
struct A {
bool a;
std::vector<std::vector<std::string>> b;
std::unordered_map<std::string, std::string> c;
std::optional<std::string> d;
std::shared_ptr<std::string> e;
double f;
} t{};
reflection::fromJson(t, R"({
"f": -3.1415926E+10,
"d": null,
"a": 0,
"c": {
"": "",
"中文Key": "中文Value",
"特殊字符!@#$%^&*()_+": "特殊Value",
"换行": "第一行\n第二行",
"制表符": "\tTabTest"
},
"b": [
[],
["单独一个"],
["多行\n字符串", "特殊字符\"\\\'", "空字符串", "\u4E2D\u6587"]
],
"e": "HelloPtr"
})");
// 断言检查
REQUIRE(t.a == false);
REQUIRE(t.f == -3.1415926E+10);
REQUIRE(!t.d.has_value());
REQUIRE(t.e != nullptr);
REQUIRE(*t.e == "HelloPtr");
REQUIRE(t.c.at("") == "");
REQUIRE(t.c.at("中文Key") == "中文Value");
REQUIRE(t.c.at("特殊字符!@#$%^&*()_+") == "特殊Value");
REQUIRE(t.c.at("换行") == "第一行\n第二行");
REQUIRE(t.c.at("制表符") == "\tTabTest");
REQUIRE(t.b.size() == 3);
REQUIRE(t.b[0].empty());
REQUIRE(t.b[1].size() == 1);
REQUIRE(t.b[1][0] == "单独一个");
REQUIRE(t.b[2].size() == 4);
REQUIRE(t.b[2][0] == "多行\n字符串");
REQUIRE(t.b[2][1] == "特殊字符\"\\\'");
REQUIRE(t.b[2][2] != "");
REQUIRE(t.b[2][3] == "中文");
}Tip
目前仅支持反射 [-128, 127] 的枚举值.
日后根据需要, 本库会迭代升级...
#include <HXLibs/reflection/EnumName.hpp>
using namespace HX;
enum MyEnum {
Z = -100,
A = 0,
B = 10,
C = 100
};
// 获取对应值的字符串
static_assert(reflection::toEnumName(static_cast<MyEnum>(0)) == "A", "");
// 从字符串反射到值
static_assert(reflection::toEnum<MyEnumClass>("Z") == MyEnumClass::Z, "");
// 支持获取枚举个数
static_assert(reflection::getValidEnumValueCnt<MyEnum>() == 4, "");
// @todo 支持获取枚举哈希表 (内部存在, 但是目前没有打算暴露)少部分情况下, 我们可能希望从 自定义类型 反射到 类型的字符串编译期常量, 则可以使用本方法.
Tip
反射 Lambda 类型 是 UB (未定义行为)
同理, 使用者 不应该 反射 带指针、引用、cv限定的类型.
#include <HXLibs/reflection/TypeName.hpp>
#include <HXLibs/log/Log.hpp>
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
using namespace HX;
namespace test {
struct Student {
struct ClassRoom {
int id;
};
std::string name;
};
template <typename T, std::size_t N>
struct Array {
T arr[N];
};
using hxArray = Array<double, 0721>;
using hxVector = Array<Array<int, 2>, 3>;
}
template <typename... Args>
struct Tmp {};
TEST_CASE("测试类型反射: struct") {
struct Student {
struct ClassRoom {
int id;
};
std::string name;
};
static_assert(reflection::getTypeName<Student>() == "Student", "");
// 只能获取到 类名, 不包含任何的 :: 嵌套
static_assert(reflection::getTypeName<Student::ClassRoom>() == "ClassRoom", "");
static_assert(reflection::getTypeName<struct Abcd>() == "Abcd", "");
// 支持命名空间
static_assert(reflection::getTypeName<test::Student>() == "Student", "");
static_assert(reflection::getTypeName<test::Student::ClassRoom>() == "ClassRoom", "");
// 支持模板
static_assert(reflection::getTypeName<Tmp<int, double, Tmp<Tmp<>>>>() == "Tmp", "");
static_assert(reflection::getTypeName<test::Array<int, 3>>() == "Array", "");
static_assert(reflection::getTypeName<test::Array<struct ONaNi, 114514>>() == "Array", "");
// 如果是别名, 只能获取到 最初始的 名称
static_assert(reflection::getTypeName<test::hxArray>() == "Array", "");
static_assert(reflection::getTypeName<test::hxVector>() == "Array", "");
}目前实现比较简单, 只是提供了颜色; 日后会支持协程、分线程的异步模式..
输出自带格式化, 特别是对于键值对和聚合类, 默认带空格 (类似于
QDebug())
@todo
Tip
WIP: 未完成, 未进行详细介绍
#include <HXLibs/db/sqlite3/MakeCreateDbSql.hpp>
#include <HXLibs/db/sqlite3/SqliteDB.hpp>
#include <HXLibs/db/sql/DataBase.hpp>
#include <HXLibs/db/sql/Param.hpp>
TEST_CASE("sqlite3/MakeCreateDbSql") {
using namespace meta::fixed_string_literals;
struct Role {
int64_t id;
int loliId;
struct UnionAttr {
attr::PrimaryKey<&Role::id, &Role::loliId> pk;
};
};
struct User {
Constraint<std::string,
attr::NotNull,
attr::DescIndex,
attr::DefaultVal<"loli"_fs>
> name;
Constraint<int,
attr::PrimaryKey<>,
attr::AutoIncrement,
attr::NotNull
> id;
Constraint<int,
attr::DefaultVal<18>
> age;
Constraint<int,
attr::Index<>
> roleId;
struct UnionAttr {
attr::Index<
attr::IndexOrder<&User::age, attr::Order::Desc>,
attr::IndexOrder<&User::roleId>
> index_1;
};
};
auto db = db::DataBase<db::DbType::Sqlite3>{"./test.db"};
// 建表
db.createDatabase<User>();
db.createDatabase<Role>();
// 插入
auto insertRes1 =
db.sqlTemplate<"插入1"_fs>()
.insert<Role>({})
.returning<&Role::id, &Role::loliId>()
.exec();
log::hxLog.info("insertRes1: data =", insertRes1.gets());
// 更新
auto updateRes2 =
db.sqlTemplate<"update_02"_fs>()
.update<&User::name>(std::string_view{"妹妹"})
.where<Col(&User::name) == "萝莉"_fs>()
.returning<&User::id, &User::name>()
.execGetChanges();
log::hxLog.info("updateRes2: datas =", updateRes2.columnRes,
", changes =", updateRes2.changes);
// 删除
auto delRes1 =
db.sqlTemplate<"del_01"_fs>()
.deleteForm<User>()
.where<Col(&User::name) == "张三"_fs>()
.returning<&User::name, &User::id>()
.execGetChanges();
log::hxLog.info("delRes1: datas =", delRes1.columnRes,
", changes =", delRes1.changes);
// 查找
auto resArr = db.sqlTemplate<"仅注册一次, 一般使用 &本函数, 即函数指针实例化一次"_fs>()
.select<Col(&User::name).as<"userId">(),
sum<Col(&User::age)>.as<"sum">()>()
.from<User>()
.where<((Col(&User::age) == db::param<int>.bind<"?age">())
|| Col(&User::age) > 2)>(cinAge)
.groupBy<&User::name>()
.exec();
for (auto&& v : resArr) {
log::hxLog.info("UserId:", v.get<"userId">(),
"AgeSum:", v.get<"sum">());
}
}对于传入参数, 支持基于名称绑定, 再也不需要数参数位置了:
TEST_CASE("sqlite3/select: bind") {
auto db = db::DataBase<DbType::Sqlite3>();
db.open("./select_test.db");
auto res = db.sqlTemplate<"sqlite3/select: bind 1"_fs>()
.select<Col(&TestSelect::id)>()
.from<TestSelect>()
.where<Col(&TestSelect::loliCnt) == param<int32_t>.bind<"?loli">()
&& Col(&TestSelect::name) == param<std::string>.bind<"?name">()
>(
bind<"?name">.link(std::string{"loli"}), // 绑定名称, 可以不按照参数顺序
bind<"?loli">.link(2233)
).exec();
}| 依赖库 | 说明 | 备注 |
|---|---|---|
| liburing | io_uring的封装 | https://github.com/axboe/liburing |
| OpenSSL 3.3.1+ | 用于https的证书/握手 | https://github.com/openssl/openssl |
Tip
- Arth Linux
- 13th Gen Intel(R) Core(TM) i9-13980HX
- RAM: 64GB
- cmake -> Release (选项
--config Release) - 测试代码: benchmarks/01_server.cpp
- 编译器: Clang 19.1.7 x86_64-pc-linux-gnu
-
http
# 全程笔记本 CPU 最高温 85度左右, 最高核心频率都在 3.5GHz
# 纯内存回复 Hello World!
╰─ wrk -d10s -t32 -c1000 http://127.0.0.1:28205/
Running 10s test @ http://127.0.0.1:28205/
32 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.84ms 10.60ms 250.54ms 94.92%
Req/Sec 73.90k 59.46k 203.26k 63.13%
22970587 requests in 10.10s, 2.91GB read
Socket errors: connect 3, read 0, write 0, timeout 0
Requests/sec: 2274737.83
Transfer/sec: 295.03MB
# 涉及I/O读写小文件 (558KB)
╰─ wrk -d10s -t32 -c1000 http://127.0.0.1:28205/html/1
Running 10s test @ http://127.0.0.1:28205/html/1
32 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.97ms 50.57ms 502.64ms 88.57%
Req/Sec 599.57 282.34 1.47k 64.59%
189702 requests in 10.10s, 100.39GB read
Socket errors: connect 3, read 189702, write 0, timeout 0
Requests/sec: 18788.05
Transfer/sec: 9.94GB
# 涉及I/O读写大文件 (574.6 MB) 断点续传, 但是 wrk 并不会测试; 故退化为普通异步文件读写
╰─ wrk -d10s -t32 -c100 http://127.0.0.1:28205/mp4
Running 10s test @ http://127.0.0.1:28205/mp4
32 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 947.45ms 451.35ms 1.98s 65.85%
Req/Sec 2.27 2.96 20.00 90.68%
376 requests in 10.09s, 225.80GB read
Socket errors: connect 0, read 376, write 0, timeout 171 # 可能: 有些请求未在 wrk 时限内完成 → 属于"还在下载中"
Requests/sec: 37.26 # 此为完成的请求个数, 不能通过此看效果, 因为显然有一些请求还在下载
Transfer/sec: 22.37GB--> C++ 编码规范
--> 开发日志