连接级 Atomic 时间戳超时的实现决策

起因 最初 Hical 的空闲超时实现就是传统做法:每个 HTTP 请求/每次 keep-alive 读等待都注册一个 steady_timer,读完成后取消,超时则关闭连接。实现上用的是 shared_ptr<function> 自引用环做回调链续期——每连接 2 次堆分配(shared_ptr 控制块 + function 对象),且每次续期都要重新构造回调。 v2.5.2 压测到 132K QPS 时,做热路径Review代码发现这个 timer 机制的问题: 每请求 2 次 epoll_ctl(注册 + 取消 timer) shared_ptr<function> 自引用环本身就有堆分配开销 140K QPS 下整体约产生 100 万次 epoll_ctl/sec,38% CPU 花在内核 _raw_spin_unlock_irqrestore(TCP spin_lock),而用户态框架代码只占不到 5% 瓶颈已经从用户态转移到内核态,减少进内核的次数成为核心策略。空闲超时的 timer 是明确可以砍掉的——30-60s 的超时精度要求本来就极低。 改良过程 分两步走: 第一步:先把 shared_ptr<function> 回调链改为独立协程 idleTimerLoop,消除自引用环和 2 次堆分配。这一步还是 per-connection 一个 timer 协程,只是实现更干净了。 第二步:发现即便使用协程,per-connection timer 仍然意味着每次 timer 到期时要走 scheduler 调度 + epoll_ctl。最终演化为"TcpServer 统一扫描"的设计——整个 server 只需要一个扫描协程,连接侧只写一个 atomic 值。 ...

May 12, 2026 · 2 min · 314 words

Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训

Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训 引言 Hical 没有用 spdlog、glog 或任何第三方日志库——整套日志系统完全自研,覆盖 20+ 个源文件:格式化、Sink 后端、文件轮转、异步双缓冲、通道分流、HTTP 集成、运行时调级。 自研日志的好处在开发心得里聊过了。这篇只聊坑——从"能跑"到"能在生产环境跑"的过程中,踩过的 8 个真实问题。 目录 Hical 踩坑实录五部曲(三):自研日志系统的 8 个血泪教训 引言 目录 坑 1:异步写盘的背压——日志不应该成为延迟来源 坑 2:双缓冲析构时丢日志 坑 3:多线程 Sink 分发的锁竞争——COW 模式的引入 坑 4:日志注入——一条恶意日志伪造十行告警 坑 5:LogAdmin 的审计致盲攻击 坑 6:TRACE 日志在 Release 构建中的隐性开销 坑 7:trace-id 生成的性能瓶颈——OpenSSL 全局锁 坑 8:文件轮转的误删风险 总结:自研日志系统的检查清单 坑 1:异步写盘的背压——日志不应该成为延迟来源 现象:压测时发现 P99 延迟间歇性飙高。排查发现不是业务逻辑慢,而是日志写入阻塞了请求线程。 根因:早期版本的日志直接在调用线程同步 fwrite。高并发下磁盘 I/O 成为瓶颈,日志调用从微秒级退化为毫秒级。 引入异步 AsyncFileSink 后,新的问题出现——如果日志产生速度远超写盘速度(比如某个 bug 触发了大量 ERROR 日志),前端缓冲区会无限增长直到 OOM。 解决方案:背压保护——缓冲区超限时主动丢弃,而非阻塞或 OOM: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // AsyncFileSink.cpp — write() 中的背压丢弃 void AsyncFileSink::write(std::string_view formattedLine) { std::lock_guard<std::mutex> lock(m_bufMutex); // 背压保护:缓冲区过大时丢弃(防止内存爆炸) if (m_curBuf.size() > m_opts.backpressureLimit) { m_dropped.fetch_add(1, std::memory_order_relaxed); return; // 丢弃这条日志,但不阻塞调用线程 } m_curBuf.append(formattedLine.data(), formattedLine.size()); // 缓冲区接近满时通知后台线程 if (m_curBuf.size() >= m_opts.bufferSize) { m_cond.notify_one(); } } 关键细节:丢弃计数不是默默吞掉的——后台线程在每次刷盘时检查丢弃计数,并将统计写入日志文件: ...

May 9, 2026 · 8 min · 1702 words

C++ Web 服务日志最佳实践:Hical 日志系统完全指南

C++ Web 服务日志最佳实践:Hical 日志系统完全指南 引子:生产环境 printf 调试?该升级了 不少 C++ 服务器项目在早期会这样写日志: 1 2 printf("[INFO] user login: uid=%d\n", uid); fprintf(stderr, "[ERROR] db connect failed\n"); 这没什么问题——直到你的服务跑到生产环境,遇到以下场景: 日志文件膨胀:跑了三天,单个 app.log 已经 8GB,grep 一下需要等几分钟 性能抖动:每次写日志都 fwrite + fflush,高并发时 I/O 成为瓶颈 信息不够:出了问题只知道"某某接口报错",不知道是哪个请求、哪个用户 无法动态调级:想临时开 DEBUG 排查问题,必须重启服务 日志散落各处:访问日志、审计日志、业务日志混在同一个文件里,难以分析 Hical 的日志系统正是为了解决这五个问题而设计的。本文从最简用法出发,逐步覆盖文件轮转、异步写盘、结构化日志、通道分流、HTTP 集成到运行时调级,每个场景都给出可直接复制的代码。 1. 快速上手:三种 API 对比 Hical 日志提供三种书写风格,适用不同场景: std::format 风格(首选) 1 2 3 4 5 #include <hical/Log.h> HICAL_LOG_INFO("server started on port={}", 8080); HICAL_LOG_WARN("connection pool low, available={}", pool.available()); HICAL_LOG_ERROR("db query failed: sql={} err={}", sql, ec.message()); 格式字符串在编译期校验(借助 std::format_string<Args...>),参数类型不匹配直接报错,不会等到运行时才崩溃。这是最常见的用法。 ...

May 6, 2026 · 6 min · 1208 words

游戏服务器的 HTTP API 层:为什么我们选择 C++ 而非 Go

游戏服务器的 HTTP API 层:为什么我们选择 C++ 而非 Go 游戏服务器迟早要暴露 HTTP API,问题不是"要不要",而是怎么加。单独起一个 Go/Python sidecar?还是直接嵌进 C++ 进程?十年以上游戏服务器开发经验告诉我——后者往往是更务实的选择。本文结合 Hical 框架的实践,聊聊背后的取舍。 游戏服务器需要 HTTP API 吗? 很多人的第一反应是"游戏用的是自定义 TCP 协议,要 HTTP 干嘛"。但在实际运营中,HTTP API 的需求无处不在: 运营 GM 工具:封号、解封、发补偿道具、改玩家数据。运营人员不会连服务器敲命令,他们需要一个 Web 界面,背后是 HTTP API。 充值回调:支付平台(微信支付、支付宝、Apple IAP)在用户付款成功后,会用 HTTP POST 通知你的服务器,这个通知必须落到游戏服务器上,否则如何给玩家加钻石? 公告系统:运营在 CMS 后台写好公告,需要一个接口通知游戏服务器"有新公告了,推给在线玩家"。 排行榜 / 战报分享:玩家把战报链接发给朋友,朋友点开是个 H5 页面,数据从游戏服务器的 HTTP 接口来。 健康检查:K8s 的 readinessProbe、运维监控系统(Prometheus、Zabbix)都期望一个 GET /health 端点,返回 200 就代表进程活着。 这些场景加在一起,游戏服务器没有 HTTP API 几乎无法正常运营。 为什么不单独起一个 Go 服务? “那我单独用 Go 写个 HTTP 服务不行吗?” 行,但会带来一系列麻烦。 ...

May 2, 2026 · 6 min · 1179 words

告别手写 API 文档:Hical OpenAPI 自动生成 + Swagger UI 一键集成

告别手写 API 文档:Hical OpenAPI 自动生成 + Swagger UI 一键集成 你是否经历过这种情况:花了半天写好 Swagger 注解,一个需求变更,参数名改了,文档却忘了同步——测试组拿着旧文档联调,来回扯皮两小时? API 文档和代码永远对不上,是后端开发者的经典痛点。本文介绍如何用 Hical 框架的 OpenAPI 模块,让文档从代码中自动生成,彻底消灭这个问题。 一、背景:OpenAPI 3.0 是什么 OpenAPI 3.0(即过去的 Swagger 规范)是描述 HTTP API 的行业标准 JSON/YAML 格式。有了它: Swagger UI / Redoc 可以直接渲染成可交互的文档页面 前端可以一键生成 TypeScript 类型定义 QA 可以直接在浏览器里填参数发请求 手写 OpenAPI YAML 很繁琐,维护成本高。Hical 的方案是:从 C++ 类型系统直接推导出 schema,标注一次,文档自动生成。 二、三步集成概览 1 2 3 步骤 1 定义 DTO,加 HICAL_JSON + HICAL_SCHEMA_NAME 步骤 2 标注路由,加 HICAL_API + builder::* 步骤 3 main() 中 registerRoutesWithOpenApi + serveOpenApi 不需要任何新依赖,OpenAPI 模块默认随 Hical 一起编译(HICAL_WITH_OPENAPI=ON 是默认值),底层复用已有的 Boost.JSON。 ...

May 1, 2026 · 5 min · 1023 words

OpenSSL RAND_bytes 完整原理:从硬件熵到密码学安全随机数

OpenSSL RAND_bytes 完整原理 从操作系统的硬件中断到你代码里的 16 字节 Session ID,随机数经历了什么? 一、为什么需要密码学安全随机数 1.1 一个真实的安全问题 Hical 框架 v1.0.0 的 Session ID 生成: 1 2 3 4 5 6 // v1.0.0(已修复) thread_local std::mt19937_64 rng(std::random_device{}()); std::uniform_int_distribution<uint64_t> dist; uint64_t hi = dist(rng); uint64_t lo = dist(rng); // 拼成 128 位十六进制 Session ID 看起来安全?不安全。 mt19937_64 是梅森旋转算法,一个确定性伪随机数生成器。攻击者只需收集 312 个连续的 64 位输出(约 156 个 Session ID),就能完全重建内部状态,预测此后所有 Session ID。 v2.0.0 的修复: 1 2 3 // v2.0.0 unsigned char buf[16]; RAND_bytes(buf, sizeof(buf)); // OpenSSL 密码学安全随机数 RAND_bytes 基于 AES-256 加密算法,即使攻击者收集到数十亿个输出,也无法预测下一个。 1.2 伪随机 vs 密码学安全随机 维度 伪随机(PRNG) 密码学安全(CSPRNG) 代表 mt19937、rand()、线性同余 RAND_bytes、getrandom(2) 内部状态 可从输出反推 不可从输出反推 前向安全 无(知道当前状态可反推历史) 有(每次输出后更新状态,旧状态不可恢复) 适用场景 模拟、游戏随机、统计抽样 密钥、Session ID、Token、Nonce 性能 ~3ns/64bit ~15ns/16bytes 标准 无 NIST SP 800-90A 判断标准:如果输出泄露后会造成安全影响(Session 劫持、密钥泄露),就必须用 CSPRNG。 ...

April 22, 2026 · 9 min · 1845 words

搭建 Hical HTTP 服务器 — 多平台环境搭建指南

搭建 Hical HTTP 服务器 — 多平台环境搭建指南 概述 本文档涵盖 Hical v2.0.0 在所有支持平台上的环境搭建,包括三种安装方式(vcpkg / Conan / 源码编译)和五个平台(Windows MSYS2、Windows MSVC、Ubuntu/Debian、Fedora/Arch、macOS)。 依赖要求 组件 版本要求 用途 C++ 编译器 GCC 14+ / Clang 22+ / MSVC 2022+ C++20 协程 + C++26 反射(可选) CMake >= 3.20 构建系统 Boost >= 1.70 Asio / Beast / JSON / System OpenSSL 必需 SSL/TLS 支持 Google Test 必需 单元测试 安装方式一:vcpkg(推荐) vcpkg 是最简单的安装方式,一行命令自动解决所有依赖。 安装 vcpkg(如未安装) 1 2 3 git clone https://github.com/microsoft/vcpkg.git cd vcpkg && bootstrap-vcpkg.bat # Windows cd vcpkg && ./bootstrap-vcpkg.sh # Linux / macOS 将 vcpkg 目录加入 PATH,或记住安装路径用于后续 CMAKE_TOOLCHAIN_FILE。 ...

April 22, 2026 · 5 min · 1018 words

C++26 前瞻心得:下一代 C++ 最值得期待的特性

C++26 前瞻心得:下一代 C++ 最值得期待的特性 C++11 让 C++ 进入现代,C++20 让 C++ 追上时代,C++26 要让 C++ 重新定义「零开销抽象」的边界。 写在前面 C++26 标准预计 2026 年底正式发布。截至本文写作时(2026 年 5 月),核心特性已基本锁定,部分编译器开始提供实验性支持。 这篇文章不追求完整列举所有提案,只聊我认为对实际项目冲击最大的特性——尤其从游戏服务器和 Hical 框架开发的角度。这是 C++17 心得 和 C++20 心得 的续篇。 声明:部分特性的最终语法可能随标准定稿而调整,代码示例基于当前最新提案。 一、静态反射(Static Reflection)—— C++ 的 Game Changer 1.1 为什么反射是最重要的 C++26 特性 在 Java、C#、Go 中习以为常的操作——遍历结构体字段、获取类名、自动序列化——在 C++ 中一直只能靠宏或代码生成。C++26 的反射(P2996)让编译器在编译期暴露类型的元信息: 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 <meta> struct Player { uint64_t id; std::string name; int level; int64_t gold; }; // 编译期遍历所有成员 template <typename T> void printFields(const T& obj) { template for (constexpr auto member : std::meta::members_of(^T)) { if constexpr (std::meta::is_nonstatic_data_member(member)) { std::println(" {}: {}", std::meta::name_of(member), obj.[:member:]); } } } Player p{1001, "Hical", 85, 999999}; printFields(p); // 输出: // id: 1001 // name: Hical // level: 85 // gold: 999999 零运行时开销,不需要宏,不需要代码生成工具,编译器原生支持。 ...

April 21, 2026 · 8 min · 1554 words

C++20 实战心得:现代 C++ 真正成熟的一代

C++20 实战心得:现代 C++ 真正成熟的一代 C++11 是革命,C++17 是打磨,C++20 是让 C++ 终于像一门「现代语言」。 写在前面 如果说 C++17 的升级是务实的,那 C++20 就是一次结构性的飞跃。协程、Concepts、Ranges、Modules——每一个都是重量级特性。但老实说,截至 2026 年,并非所有特性都已经在生产环境中稳定好用。 这篇文章从我在游戏服务器和 Hical 框架开发中的实际使用出发,聊聊哪些 C++20 特性已经值得用、哪些还需要等等。 一、Concepts —— 模板错误信息终于能看懂了 1.1 C++20 之前的模板报错 先感受一下 C++17 时代的"恐怖": 1 2 std::list<int> lst; std::sort(lst.begin(), lst.end()); GCC 会喷出几十行模板展开错误,核心意思是 std::list::iterator 不是随机访问迭代器——但你得从一堆 __normal_iterator、__gnu_cxx 嵌套模板中自己悟出来。 1.2 Concepts:把约束说人话 1 2 3 4 5 6 7 8 9 template <std::random_access_iterator Iter> void mySort(Iter first, Iter last) { // ... } std::list<int> lst; mySort(lst.begin(), lst.end()); // 错误信息:约束 'random_access_iterator' 不满足 // 一行,清清楚楚 Concepts 的本质:给模板参数加上编译期的「类型契约」。SFINAE 能做的它都能做,但写法是人能读懂的。 ...

April 20, 2026 · 8 min · 1683 words

C++17 实战心得:那些真正改变我写代码方式的特性

C++17 实战心得:那些真正改变我写代码方式的特性 从游戏服务器开发的视角出发,不求面面俱到,只聊那些真正让我「回不去了」的 C++17 特性。 写在前面 C++17 的特性列表很长,但实际工作中高频使用的并不多。这篇文章只聊我在游戏服务器开发中真正用上了、且明显感到提升的特性,按「爽度」排序。 一、结构化绑定(Structured Bindings) 1.1 告别 .first / .second C++17 之前,遍历 std::map 是这样的: 1 2 3 4 5 for (auto it = playerMap.begin(); it != playerMap.end(); ++it) { auto playerId = it->first; auto& player = it->second; // ... } C++17 之后: 1 2 3 for (auto& [playerId, player] : playerMap) { LOG_DEBUG << "玩家 " << playerId << " 等级: " << player.level; } 一行搞定,变量名直接表达语义,可读性提升巨大。 1.2 配合 insert / emplace 的返回值 1 2 3 4 auto [iter, success] = onlinePlayers.emplace(playerId, std::move(session)); if (!success) { LOG_WARN << "玩家 " << playerId << " 重复登录"; } 比起 result.second 去判断是否插入成功,success 的语义一目了然。 1.3 多返回值函数 1 2 // 解析网络包头:返回包类型和包体长度 auto [msgType, bodyLen] = parsePacketHeader(buffer); 不用再纠结「该用 std::pair 还是定义一个临时结构体」的问题了。当然,如果返回值超过 3 个,还是老老实实定义结构体。 ...

April 19, 2026 · 5 min · 1014 words