用 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

深入学习 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

深入学习 io_uring(二):高级特性与 TCP 网络编程

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 已阅读入门篇,理解 SQ/CQ 双环形缓冲区和 liburing 基本 API 熟悉 TCP socket 编程基础(socket、bind、listen、accept) 1. SQPOLL:零系统调用提交 1.1 常规模式的瓶颈 入门篇中每次 io_uring_submit() 底层都会调用 io_uring_enter() 系统调用: 1 2 3 4 5 6 7 8 9 10 常规模式: 用户态 内核态 │ │ │ SQE 写入共享内存 │ │ │ │ io_uring_enter(to_submit=N) │ ├────────系统调用──────────────→│ ← 仍有上下文切换 │ │ 读取 SQ,执行 I/O │ 返回 │ │←─────────────────────────────┤ 对于超高频提交场景(如高频交易、高吞吐数据库),连这一次系统调用都嫌多。 ...

October 2, 2025 · 15 min · 3155 words

深入学习 io_uring(一):从原理到第一个异步程序

系列导航:入门篇 | 进阶篇 | 实战篇 引言:epoll 之后,还能更快吗? 假设你用 epoll 写了一个高并发 TCP 服务器,性能已经不错——C10K 问题解决了。但当你把连接数推到 C1M(百万级) 时: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 epoll 的瓶颈: 用户态 内核态 │ │ │ epoll_wait() │ ├───────系统调用─────────────→│ ← 每次至少一次上下文切换 │ │ │ 返回就绪的 fd 列表 │ │←───────────────────────────┤ │ │ │ recv(fd_1, buf, ...) │ ├───────系统调用─────────────→│ ← 每个 fd 又一次系统调用! │ │ │ send(fd_1, resp, ...) │ ├───────系统调用─────────────→│ ← 再一次! │ │ │ recv(fd_2, buf, ...) │ ├───────系统调用─────────────→│ ← N 个连接 = 2N+ 次系统调用 问题:epoll 只解决了"哪些 fd 就绪"的问题,每次 I/O 操作仍然需要独立的系统调用。百万连接下,系统调用的开销成为主要瓶颈——上下文切换、数据拷贝、内核锁竞争。 ...

October 1, 2025 · 13 min · 2737 words