用 C++20 Concepts 设计可替换的网络后端:从 Boost.Asio 到未来的 io_uring

用 C++20 Concepts 设计可替换的网络后端:从 Boost.Asio 到未来的 io_uring 本文以 Hical 框架为例,展示如何用 C++20 Concepts 约束网络后端接口,实现编译期类型安全的后端抽象。 问题:网络后端绑定的困境 大多数 C++ 网络框架和底层网络库深度绑定。Drogon 绑定 Trantor,muduo 绑定自研的 EventLoop。一旦想换后端(比如从 epoll 切到 io_uring),基本等于重写。 原因是传统的抽象手段——虚函数继承——有两个问题: 运行时开销:每次调用都经过 vtable 接口松散:基类定义了接口,但"你的实现是否真的完整?“只能在链接期或运行时才知道 Concepts:编译期的接口约束 C++20 Concepts 提供了一种具名约束机制——在编译期验证类型是否满足一组要求: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename T> concept EventLoopLike = requires(T loop, std::function<void()> func, double delay) { { loop.run() } -> std::same_as<void>; { loop.stop() } -> std::same_as<void>; { loop.isRunning() } -> std::convertible_to<bool>; { loop.post(func) } -> std::same_as<void>; { loop.dispatch(func) } -> std::same_as<void>; { loop.runAfter(delay, func) } -> std::convertible_to<uint64_t>; { loop.runEvery(delay, func) } -> std::convertible_to<uint64_t>; { loop.cancelTimer(uint64_t{}) } -> std::same_as<void>; { loop.isInLoopThread() } -> std::convertible_to<bool>; { loop.index() } -> std::convertible_to<size_t>; { loop.allocator() } -> std::same_as<std::pmr::polymorphic_allocator<std::byte>>; }; 如果某个类型缺少 run() 方法或返回类型不对,编译器立即报错,而不是在链接时给出晦涩的"未定义引用”。 ...

April 12, 2026 · 3 min · 580 words

深入学习 C++20 协程(Coroutines)

深入学习 C++20 协程(Coroutines) 头文件:<coroutine> 命名空间:std 编译器要求:GCC 11+ / Clang 14+ / MSVC 19.28+(均需 -std=c++20 或以上) 注意:GCC 10 / Clang 8~13 可通过 -fcoroutines 和 <experimental/coroutine> 使用实验性支持 一、为什么需要协程? 1.1 异步编程的传统痛点 游戏服务器中充斥着异步操作——数据库查询、网络 I/O、定时器回调。传统方案各有各的痛: 方案 A:回调地狱(Callback Hell) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void HandleLogin(Connection* conn, const LoginPacket& pkt) { // 第1步:查询数据库验证账号 dbManager->QueryAsync("SELECT * FROM accounts WHERE name=?", pkt.name, [conn, pkt](const DBResult& result) { if (!result.ok) { conn->SendError("DB错误"); return; } // 第2步:查询角色列表 dbManager->QueryAsync("SELECT * FROM characters WHERE account_id=?", result.accountId, [conn](const DBResult& charResult) { if (!charResult.ok) { conn->SendError("DB错误"); return; } // 第3步:加载角色数据 dbManager->QueryAsync("SELECT * FROM inventory WHERE char_id=?", charResult.charId, [conn, charResult](const DBResult& invResult) { // 第4步:终于可以发送登录成功了... conn->SendLoginSuccess(charResult, invResult); }); }); }); } 方案 B:状态机(State Machine) ...

April 8, 2026 · 22 min · 4611 words

深入学习 io_uring(三):C++ 封装、协程集成与高性能架构

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 已阅读入门篇和进阶篇,掌握 io_uring 双环形缓冲区和 liburing API 了解 C++20 协程基础(co_await、coroutine_handle、promise_type) 建议先阅读 深入学习 Boost.Asio(三):实战篇 中的协程部分作为对照 1. RAII 封装:安全管理 io_uring 资源 1.1 为什么需要 C++ 封装 直接使用 liburing 的 C API 有三个痛点: io_uring_queue_init / io_uring_queue_exit 手动配对,容易遗漏 user_data 是 void* 或 uint64_t,类型安全全靠人肉 提交→收割的事件循环代码高度模板化,每个项目重写一遍 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 封装层次: 应用代码(协程/回调) │ ▼ ┌──────────────────────┐ │ IoUringAwaitable │ ← 协程集成层(co_await 一个 I/O 操作) └──────────────────────┘ │ ▼ ┌──────────────────────┐ │ IoUringContext │ ← 事件循环层(submit / wait / dispatch) └──────────────────────┘ │ ▼ ┌──────────────────────┐ │ IoUring (RAII) │ ← 资源管理层(init / exit) └──────────────────────┘ │ ▼ liburing C API 1.2 IoUring:RAII 包装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 // IoUring.hpp — RAII 封装 io_uring 实例 // 编译:g++ -std=c++20 -O2 xxx.cpp -luring -o xxx #pragma once #include <liburing.h> #include <stdexcept> #include <string> #include <cstring> class IoUring { public: // 构造时初始化 io_uring,指定 SQ 大小和可选标志 explicit IoUring(unsigned entries, unsigned flags = 0) { int ret = io_uring_queue_init(entries, &ring_, flags); if (ret < 0) { throw std::runtime_error( "io_uring_queue_init 失败: " + std::string(strerror(-ret))); } } // 禁止拷贝(io_uring 资源不可共享) IoUring(const IoUring&) = delete; IoUring& operator=(const IoUring&) = delete; // 允许移动 IoUring(IoUring&& other) noexcept : ring_(other.ring_) { other.moved_ = true; } // 析构时自动清理 ~IoUring() { if (!moved_) { io_uring_queue_exit(&ring_); } } // 获取 SQE(SQ 满时自动 submit 腾出空间) io_uring_sqe* getSqe() { io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (!sqe) { // SQ 满了,先提交当前积压的请求 io_uring_submit(&ring_); sqe = io_uring_get_sqe(&ring_); if (!sqe) { throw std::runtime_error("SQ 空间不足,即使 submit 后仍无法获取 SQE"); } } return sqe; } int submit() { return io_uring_submit(&ring_); } // 阻塞等待至少一个 CQE io_uring_cqe* waitCqe() { io_uring_cqe* cqe = nullptr; int ret = io_uring_wait_cqe(&ring_, &cqe); if (ret < 0) { throw std::runtime_error( "io_uring_wait_cqe 失败: " + std::string(strerror(-ret))); } return cqe; } // 非阻塞查看 CQE io_uring_cqe* peekCqe() { io_uring_cqe* cqe = nullptr; int ret = io_uring_peek_cqe(&ring_, &cqe); if (ret == -EAGAIN) return nullptr; // 无就绪 CQE if (ret < 0) { throw std::runtime_error( "io_uring_peek_cqe 失败: " + std::string(strerror(-ret))); } return cqe; } // 标记 CQE 已消费 void seenCqe(io_uring_cqe* cqe) { io_uring_cqe_seen(&ring_, cqe); } // 访问底层 io_uring(高级用法需要) io_uring* raw() { return &ring_; } private: io_uring ring_{}; bool moved_ = false; }; 设计原则:RAII 保证 io_uring_queue_exit 一定被调用,即使异常传播也不会泄漏内核资源。getSqe() 中自动 submit 是防御性编程——避免 SQ 满导致的隐性 bug。 ...

October 3, 2025 · 14 min · 2887 words

深入学习 Boost.Asio(三):协程进阶与实战项目

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 阅读本篇前,请确保已掌握: 入门篇:io_context、异步操作生命周期、定时器 进阶篇:协程 Echo Server、多线程模型、strand 1. 协程进阶技巧 1.1 co_spawn 的第三个参数 co_spawn 的第三个参数决定了协程完成后的行为: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <boost/asio.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/use_awaitable.hpp> using boost::asio::awaitable; using boost::asio::use_awaitable; awaitable<int> compute() { co_return 42; } awaitable<void> mayFail() { throw std::runtime_error("oops"); co_return; } void examples(boost::asio::io_context& ioCtx) { // 方式1:detached —— 忽略返回值和异常 // 适用:独立运行的协程(如连接处理) boost::asio::co_spawn(ioCtx, compute(), boost::asio::detached); // 方式2:回调 —— 协程完成时执行回调 // 适用:需要捕获协程异常或获取返回值 boost::asio::co_spawn(ioCtx, mayFail(), [](std::exception_ptr e) { if (e) { try { std::rethrow_exception(e); } catch (const std::exception& ex) { std::cerr << "协程异常: " << ex.what() << "\n"; } } }); // 方式3:use_awaitable —— 在协程中等待另一个协程 // 适用:父子协程关系 // (需要在协程内使用) } // 方式3 完整示例 awaitable<void> parent(boost::asio::io_context& ioCtx) { // 等待子协程完成并获取返回值 int result = co_await boost::asio::co_spawn( ioCtx, compute(), boost::asio::use_awaitable); std::cout << "子协程返回: " << result << "\n"; // 42 } 1.2 超时控制 生产环境中,你不能无限等待一个操作完成。Asio 提供了 awaitable_operators 实现竞争式等待: ...

May 21, 2025 · 10 min · 1926 words

深入学习 std::span

深入学习 std::span 头文件:<span> 命名空间:std 编译器要求:C++20 起 一、设计动机:统一连续内存的访问接口 1.1 C++ 中连续内存的 N 种传参方式 在没有 span 之前,传递"一段连续内存"的方式五花八门: 1 2 3 4 5 6 7 8 9 10 11 12 13 // 方式1:C 风格——指针 + 长度(容易出错,长度可能传错) void process(const int* data, size_t len); // 方式2:模板——编译膨胀,每种容器实例化一份 template <typename Container> void process(const Container& c); // 方式3:特化 vector 引用——不接受 array 或 C 数组 void process(const std::vector<int>& v); // 方式4:迭代器对——语法啰嗦,不直观 template <typename Iter> void process(Iter begin, Iter end); 核心问题: 没有一种统一的、类型安全的方式说"我只需要一段连续内存的只读/可写视图"。 1.2 span 的解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <span> #include <vector> #include <array> #include <cstdio> // ✅ 一个函数接受所有连续内存容器 void process(std::span<const int> data) { for (int val : data) { printf("%d ", val); } printf("\n"); } int main() { // span 能从任何连续内存容器隐式构造 std::vector<int> vec = {1, 2, 3, 4, 5}; std::array<int, 3> arr = {10, 20, 30}; int cArr[] = {100, 200, 300, 400}; process(vec); // vector → span:隐式转换 process(arr); // array → span:隐式转换 process(cArr); // C 数组 → span:隐式转换 process({vec.data() + 1, 3}); // 子区间:手动指定 {ptr, count} } 一句话总结:span 是连续内存的"通用视图"——不拥有数据、不分配内存、只是指针+长度的薄封装。 ...

May 25, 2022 · 10 min · 1921 words