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

深入学习 Boost.Asio(二):TCP 编程与多线程模型

系列导航:入门篇 | 进阶篇 | 实战篇 前置知识 阅读本篇前,请确保已理解 入门篇 中的以下概念: io_context 的作用和 run() 执行流程 异步操作的生命周期(发起 → 完成 → handler 执行) post/dispatch 的区别 1. TCP 编程:三步演进 我们通过构建一个 Echo Server(收到什么就回什么),从最简单的同步版本逐步演进到生产级协程版本。 1.1 第一步:同步阻塞版 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 // echo_server_sync.cpp // 编译: g++ -std=c++20 echo_server_sync.cpp -lboost_system -lpthread -o echo // 测试: 另开终端 nc localhost 9999,输入文字会回显 #include <boost/asio.hpp> #include <iostream> using boost::asio::ip::tcp; int main() { boost::asio::io_context ioCtx; // 创建 acceptor:监听 TCP 连接 // 参数:io_context, 绑定地址(IPv4, 端口9999) tcp::acceptor acceptor(ioCtx, tcp::endpoint(tcp::v4(), 9999)); std::cout << "同步 Echo Server 监听端口 9999\n"; while (true) { // accept() 阻塞,直到有客户端连接 tcp::socket socket(ioCtx); acceptor.accept(socket); std::cout << "客户端连接: " << socket.remote_endpoint().address().to_string() << ":" << socket.remote_endpoint().port() << "\n"; // 处理这个连接(阻塞:处理期间无法接受新连接!) boost::system::error_code ec; char buf[1024]; while (true) { // read_some:读取可用的数据(可能只有一部分) size_t n = socket.read_some(boost::asio::buffer(buf), ec); if (ec == boost::asio::error::eof) { std::cout << "客户端断开\n"; break; } if (ec) throw boost::system::system_error(ec); // 将收到的数据原样写回 boost::asio::write(socket, boost::asio::buffer(buf, n)); } } return 0; } 问题:同一时刻只能服务一个客户端。当客户端 A 连接后,客户端 B 必须等 A 断开才能被接受。 ...

May 20, 2025 · 10 min · 2026 words

深入学习 Boost.Asio(一):从原理到 io_context

系列导航:入门篇 | 进阶篇 | 实战篇 引言:为什么需要异步 I/O? 假设你在写一个聊天服务器,同时连接 1000 个用户。如果用传统的"一个线程处理一个连接"模型: 1 2 3 4 线程1: read(socket_1) ← 阻塞等待用户1输入... 线程2: read(socket_2) ← 阻塞等待用户2输入... ... 线程1000: read(socket_1000) ← 阻塞等待用户1000输入... 问题:1000 个线程各自阻塞在 read() 上,每个线程占用 ~1MB 栈内存(合计 ~1GB),还有大量的上下文切换开销。这就是经典的 C10K 问题。 异步 I/O 的解决思路:用 1 个线程(或少量线程)管理所有连接,操作系统在数据就绪时通知我们: 1 2 3 4 5 6 单线程事件循环: ┌→ 等待事件(epoll/IOCP) │ ├─ socket_7 可读 → 处理用户7的消息 │ ├─ socket_42 可读 → 处理用户42的消息 │ └─ socket_100 可写 → 继续发送给用户100 └─ 回到等待 Boost.Asio 就是 C++ 中实现这一模型的工业级库。本篇将带你理解它的底层原理和核心组件。 ...

May 18, 2025 · 10 min · 1990 words

trantor 网络库学习总结

trantor 网络库学习总结 学习周期:一个多月 覆盖范围:trantor 全部核心模块,共 18 课 一、整体架构鸟瞰 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 ┌─────────────────────────────────────────────────────────┐ │ 用户代码 / Drogon 框架 │ ├─────────────────────────────────────────────────────────┤ │ TcpServer / TcpClient │ │ • 连接管理(connSet_) • Round-Robin 分配 │ │ • TimingWheel 超时 • promise/future 优雅停止 │ ├────────────────┬────────────────────────────────────────┤ │ TcpConnection │ TaskQueue(Serial / Concurrent) │ │ • 状态机 │ • 卸载阻塞操作 │ │ • 发送队列 │ • SerialTaskQueue = EventLoopThread │ │ • TLS透明层 │ • ConcurrentTaskQueue = 线程池 │ ├────────────────┴────────────────────────────────────────┤ │ EventLoopThread / EventLoopThreadPool │ │ • 3阶段 promise/future 启动协议 │ │ • atomic round-robin 无锁分配 │ ├──────────┬──────────────┬──────────────────────────────┤ │ Acceptor │ Connector │ Resolver(DNS 异步解析) │ │ idleFd_ │ EINPROGRESS │ • NormalResolver(线程池) │ │ EMFILE │ 指数退避 │ • AresResolver(c-ares) │ ├──────────┴──────────────┴──────────────────────────────┤ │ EventLoop(Reactor 核心) │ │ loop() ← Channel ← Poller(epoll/kqueue/IOCP) │ │ runInLoop / queueInLoop / runAfter / runEvery │ │ MpscQueue<Func>:无锁任务投递 │ ├──────────────────────────┬──────────────────────────────┤ │ 定时器系统 │ 工具层 │ │ TimerQueue(最小堆) │ MsgBuffer / Logger │ │ TimingWheel(O(1) 超时) │ ObjectPool / MpscQueue │ │ timerfd / wakeupFd 驱动 │ Hash / secureRandomBytes │ └──────────────────────────┴──────────────────────────────┘ ↓ OS:epoll / kqueue / IOCP / wepoll 二、18 课核心知识点速查 阶段一:基础工具层(第 1-4 课) 第 1 课 — 日志系统 LOG_INFO << "msg" 展开为 Logger(__FILE__, __LINE__).stream(),析构时刷出 FixedBuffer<N>:栈上固定缓冲,避免日志路径的堆分配 AsyncFileLogger:前台线程写入内存队列,后台线程批量刷盘(异步、不阻塞 I/O) 自定义输出:Logger::setOutputFunction(),可对接 ELK、syslog 等 第 2 课 — 消息缓冲区 MsgBuffer 双指针设计:_readIndex / _writeIndex,中间是可读数据,右侧是可写空间 prepend 区域(8字节):预留报头空间,避免插入时移动数据 readFd():readv + 栈上 65536 字节备用缓冲,单次 syscall 读取大量数据 BufferNode 4种子类:MemBufferNode、FileBufferNodeUnix、FileBufferNodeWin、AsyncStreamBufferNode 第 3 课 — 日期时间与工具函数 Date:微秒精度时间点(int64_t microSecondsSinceEpoch_),可作定时器 key Date::now() → gettimeofday / GetSystemTimeAsFileTime NonCopyable:= delete 拷贝构造和赋值,所有核心类的基类 第 4 课 — 回调类型定义 ConnectionCallback:连接建立/断开 RecvMessageCallback:收到数据(TcpConnectionPtr + MsgBuffer*) WriteCompleteCallback:发送缓冲区清空 TimerCallback:定时器触发 阶段二:Reactor 核心(第 5-8 课) 第 5 课 — EventLoop 核心循环:epoll_wait → 分发 Channel 事件 → 执行 pendingFunctors_ wakeupFd_(eventfd/pipe):跨线程唤醒阻塞的 epoll_wait runInLoop(f):当前线程直接执行;其他线程 → queueInLoop → 唤醒 → 下轮执行 MpscQueue<Func> funcs_:任务队列用无锁 MPSC 队列,多线程投递无锁 关键不变量:EventLoop 是单线程的,所有网络操作必须在其线程执行。 ...

April 15, 2025 · 6 min · 1152 words

密码学工具 — 哈希函数 & 安全随机数

第 18 课:密码学工具 — 哈希函数 & 安全随机数 对应源文件: trantor/utils/Utilities.h — 公开 API(Hash128/160/256、所有哈希函数、secureRandomBytes) trantor/utils/Utilities.cc — 无 TLS 后端时的纯 C 实现 trantor/utils/crypto/openssl.cc — OpenSSL 后端实现 trantor/utils/crypto/botan.cc — Botan 后端实现 trantor/utils/crypto/md5.h/cc — 内置 MD5(纯 C) trantor/utils/crypto/sha1.h/cc — 内置 SHA1(纯 C,公有域) trantor/utils/crypto/sha256.h/cc — 内置 SHA256(纯 C) trantor/utils/crypto/sha3.h/cc — 内置 SHA3-256(Keccak,纯 C) trantor/utils/crypto/blake2.h/cc — 内置 BLAKE2b-256(纯 C) 一、整体架构:三层后端选择 trantor 的密码学工具采用编译期后端切换设计,同一套 API 在三种环境下对应不同实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 用户代码 │ ▼ trantor::utils::md5(data, len) ← 统一 API(Utilities.h) │ ├─ USE_OPENSSL 定义时 ──→ crypto/openssl.cc (OpenSSL EVP API) ├─ USE_BOTAN 定义时 ──→ crypto/botan.cc (Botan HashFunction) └─ 两者均无时 ──────────→ Utilities.cc (内置纯 C 实现) ├─ crypto/md5.cc ├─ crypto/sha1.cc ├─ crypto/sha256.cc ├─ crypto/sha3.cc └─ crypto/blake2.cc 设计原理: ...

April 12, 2025 · 11 min · 2189 words

并发工具 — MpscQueue 无锁队列 & ObjectPool 对象池

第17 课:并发工具 — MpscQueue 无锁队列 & ObjectPool 对象池 对应源文件: trantor/utils/LockFreeQueue.h — MpscQueue<T>:无锁多生产者单消费者队列 trantor/utils/ObjectPool.h — ObjectPool<T>:共享指针驱动的对象池 一、为什么需要这两个工具? 在高性能网络库中,有两类性能瓶颈反复出现: 瓶颈 1:跨线程任务投递 EventLoop::runInLoop() 每秒可能被调用数十万次(每个连接的读写完成都要回调)。如果用 mutex 保护投递队列,大量线程争锁会造成显著延迟。 → 解决方案:MpscQueue<T> 无锁队列,多线程投递不需要 mutex。 瓶颈 2:高频内存分配 HTTP 框架每个请求都要 new HttpRequest、new HttpResponse,请求完成立刻 delete。频繁的堆分配/释放不仅慢,还会造成内存碎片。 → 解决方案:ObjectPool<T> 对象池,释放时归还而不销毁,下次直接复用。 二、MpscQueue — 无锁多生产者单消费者队列 2.1 完整实现(共 ~115 行) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename T> class MpscQueue : public NonCopyable { private: struct BufferNode { BufferNode() = default; BufferNode(const T &data) : dataPtr_(new T(data)) {} BufferNode(T &&data) : dataPtr_(new T(std::move(data))) {} T *dataPtr_; // 堆上的数据 std::atomic<BufferNode *> next_{nullptr}; }; std::atomic<BufferNode *> head_; // 指向最新插入的节点 std::atomic<BufferNode *> tail_; // 指向最旧的哨兵节点(消费端) }; 2.2 初始状态 1 2 3 4 MpscQueue() : head_(new BufferNode), // 创建哨兵节点 tail_(head_.load(std::memory_order_relaxed)) // tail = head = 哨兵 {} 初始状态: ...

April 10, 2025 · 10 min · 1947 words

DNS 解析 — Resolver、NormalResolver、AresResolver

第 16 课:DNS 解析 — Resolver、NormalResolver、AresResolver 对应源文件: trantor/net/Resolver.h — 异步 DNS 解析抽象接口 trantor/net/inner/NormalResolver.h / NormalResolver.cc — 基于 getaddrinfo 的线程池实现 trantor/net/inner/AresResolver.h — 基于 c-ares 的真异步实现 一、为什么 DNS 解析需要异步? getaddrinfo() 是系统标准 DNS 解析函数,但它是阻塞的——在 DNS 服务器响应之前,调用线程会一直挂起。 在 EventLoop 单线程模型中,如果直接调用 getaddrinfo(): EventLoop 线程被阻塞 该线程上所有其他连接的 I/O 事件、定时器全部停止响应 哪怕只是 100ms 的 DNS 查询,对游戏服务器来说都是灾难性的抖动 trantor 提供两种解决方案: 1 2 3 4 5 6 7 8 9 10 11 12 DNS 解析请求 │ ├── NormalResolver(默认) │ → 投递到 ConcurrentTaskQueue(阻塞线程池) │ → getaddrinfo() 在工作线程里阻塞 │ → 完成后回调(在工作线程里直接调用) │ └── AresResolver(c-ares 可用时) → 在 EventLoop 线程里全程非阻塞 → c-ares 管理 DNS socket,注册到 epoll → EventLoop 处理 DNS socket 的读写事件 → 完成后在 EventLoop 线程里调用回调 二、Resolver — 统一抽象接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Resolver { public: using Callback = std::function<void(const trantor::InetAddress&)>; using ResolverResultsCallback = std::function<void(const std::vector<trantor::InetAddress>&)>; // 工厂函数:根据编译配置选择实现 static std::shared_ptr<Resolver> newResolver( EventLoop *loop = nullptr, size_t timeout = 60); // timeout:DNS 缓存有效期(秒) // 解析单个地址 virtual void resolve(const std::string& hostname, const Callback& callback) = 0; // 解析所有地址(A/AAAA 记录,可能有多个 IP) virtual void resolve(const std::string& hostname, const ResolverResultsCallback& callback) = 0; static bool isCAresUsed(); // 当前是否在用 c-ares }; 两个回调的区别: ...

April 5, 2025 · 10 min · 1921 words

TLS 安全通信

第 15 课:TLS 安全通信 对应源文件: trantor/net/TLSPolicy.h — TLS 策略配置(证书、验证规则、ALPN 等) trantor/net/Certificate.h — 证书抽象接口 trantor/net/inner/TLSProvider.h — TLS 提供者抽象基类(策略模式) 具体实现(未深入):OpenSSLProvider.cc(OpenSSL 后端)、BotanTLSProvider.cc(Botan 后端) 一、TLS 在 trantor 中的架构 trantor 的 TLS 是完全透明的——插入到 TcpConnectionImpl 和用户代码之间,用户几乎感知不到加密的存在: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 用户代码(send/recvMsgCallback) │ ▲ │ 明文数据 │ 解密后的明文 ▼ │ ┌───────────────────────────┐ │ TLSProvider │ ← 透明加密/解密层 │ startEncryption() │ │ sendData(明文) → 密文 │ │ recvData(密文) → 明文 │ └───────────────────────────┘ │ ▲ │ TLS 密文 │ 从 socket 读到的密文 ▼ │ TcpConnectionImpl(writeRaw / readBuffer_) │ ▼ TCP socket(内核) 策略模式(Strategy Pattern) TLSProvider 是一个纯虚接口,具体的 TLS 实现(OpenSSL、Botan)是策略类: ...

April 1, 2025 · 9 min · 1902 words

任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue

第 14 课:任务队列 — TaskQueue、SerialTaskQueue、ConcurrentTaskQueue 对应源文件: trantor/utils/TaskQueue.h — 抽象基类 trantor/utils/SerialTaskQueue.h / SerialTaskQueue.cc — 串行执行队列 trantor/utils/ConcurrentTaskQueue.h / ConcurrentTaskQueue.cc — 并发线程池队列 一、为什么需要 TaskQueue? EventLoop 是单线程的,其中不能执行任何阻塞操作(数据库查询、文件 I/O、耗时计算),否则整条链路的 I/O 响应都会被拖慢。 TaskQueue 提供了一个"卸载阻塞任务"的机制: 1 2 3 4 5 6 [EventLoop 线程] [TaskQueue 线程] 收到玩家请求 │ → 投递到 TaskQueue ────────────►│ 执行 DB 查询(可阻塞) → 立即返回,处理下一个事件 │ 查询完成 │ → 回调投递回 EventLoop ◄── 收到结果,发送响应 ────────────┘ 这是异步编程的基本模式:不阻塞事件循环,把耗时操作委托给专用线程。 二、TaskQueue — 抽象基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class TaskQueue : public NonCopyable { public: // 纯虚:子类实现具体的投递方式 virtual void runTaskInQueue(const std::function<void()> &task) = 0; virtual void runTaskInQueue(std::function<void()> &&task) = 0; virtual std::string getName() const { return ""; } // 同步执行:投递任务并阻塞等待完成(基类实现,子类免费获得) void syncTaskInQueue(const std::function<void()> &task) { std::promise<int> prom; std::future<int> fut = prom.get_future(); runTaskInQueue([&]() { task(); prom.set_value(1); // 任务完成,解锁调用方 }); fut.get(); // 阻塞等待 } }; syncTaskInQueue 的精妙之处: ...

March 29, 2025 · 8 min · 1504 words

多线程 EventLoop — EventLoopThread & EventLoopThreadPool

第 13 课:多线程 EventLoop — EventLoopThread & EventLoopThreadPool 对应源文件: trantor/net/EventLoopThread.h / EventLoopThread.cc — 在独立线程运行一个 EventLoop trantor/net/EventLoopThreadPool.h / EventLoopThreadPool.cc — EventLoop 线程池 一、为什么需要这两个类? 在第 12 课里,我们看到 TcpServer::setIoLoopNum(4) 内部创建了一个 EventLoopThreadPool。它们解决的核心问题是: 如何安全地在新线程里创建 EventLoop,并确保 EventLoop 真正开始运行后再返回给调用者? 这看起来简单,实际上有一个微妙的同步问题: EventLoop 对象必须在它将要运行的线程里创建(t_loopInThisThread 线程局部变量) 调用者拿到 EventLoop * 之前,要保证该指针有效(对象已创建) run() 返回之前,要保证 EventLoop 确实进入了 loop() 主循环(否则第一个 runInLoop 可能无法立刻执行) trantor 用三个 std::promise 精确解决了这个三阶段同步问题。 二、EventLoopThread 的三阶段启动协议 2.1 成员变量一览 1 2 3 4 5 6 7 8 std::shared_ptr<EventLoop> loop_; // EventLoop 对象(新线程里创建) std::mutex loopMutex_; // 保护 loop_ 的读写(析构时用) std::string loopThreadName_; // 线程名(prctl 设置) std::promise<std::shared_ptr<EventLoop>> promiseForLoopPointer_; // ① EventLoop 指针就绪 std::promise<int> promiseForRun_; // ② "请开始循环"信号 std::promise<int> promiseForLoop_; // ③ "循环已在运行"确认 std::once_flag once_; // 保证 run() 只执行一次 std::thread thread_; // 实际的 OS 线程 2.2 三阶段时序图 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 主线程 新线程(loopFuncs) │ │ │ EventLoopThread(name) │ │ → thread_ = std::thread(loopFuncs) │ 线程启动 │ → f = promiseForLoopPointer_.get_future() │ │ ↓ 阻塞等待 ① │ prctl(PR_SET_NAME) │ │ loop = make_shared<EventLoop>() │ 【①】│ promiseForLoopPointer_.set_value(loop) │ ← f.get() 返回 loop 指针 │ │ → this->loop_ = loop │ promiseForLoop_: queueInLoop 注册回调 │ │ f2 = promiseForRun_.get_future() │ (构造完成,loop_ 有效,但循环未开始) │ ↓ 阻塞等待 ② │ │ │ run() │ │ → std::call_once { │ │ f3 = promiseForLoop_.get_future() │ │ 【②】promiseForRun_.set_value(1) │ │ ↓ 阻塞等待 ③ │ ← f2.get() 返回 │ │ loop->loop() 开始 │ │ → 第一次 poll │ │ → doRunInLoopFuncs() │ 【③】│ → promiseForLoop_.set_value(1) │ ← f3.get() 返回 │ (循环持续运行...) │ } │ (run() 返回,EventLoop 确保在运行中) 三个 promise 的职责: ...

March 28, 2025 · 9 min · 1757 words