优先选 asio:其 io_context + strand 模型可控、c++17 协程友好、内存行为可预测;libuv 的 c 接口在 c++ 中易引发生命周期问题。

用 asio 还是 libuv?别纠结,先看并发模型
游戏服务器不是 Web 服务,不能直接套用 REST 框架。核心矛盾是:单连接要维持长状态(角色、位置、CD)、高频率收发(每秒 10–30 帧)、低延迟敏感(>200ms 就卡顿)。所以选底层网络库时,重点不是“功能多”,而是「事件循环是否可控」「内存分配是否可预测」「线程模型是否贴合业务分层」。
asio(尤其是 boost::asio 1.70+ 的 io_context + strand)对 C++ 游戏服更友好:C++17 后原生支持协程(co_await),配合 use_awaitable 能写出接近同步逻辑的异步代码;而 libuv 是 C 接口,C++ 封装层容易引入隐式拷贝或生命周期陷阱。
- 如果用
asio,禁用thread_pool,手动管理io_context::run()线程数(通常 1 个主线程 + N 个工作线程,N ≤ CPU 核心数 − 1) - 避免在
async_read回调里做耗时操作(如 DB 查询),必须拆成「收包 → 投递到工作队列 → 异步处理 → 回包」三段 - 不要用
asio::spawn(已弃用),改用asio::co_spawn+asio::detached或自定义awaitable调度器
flatbuffers 替代 protobuf?协议序列化必须零拷贝
游戏协议不是日志,不能容忍序列化/反序列化时的 vector 分配和字符串拷贝。protobuf 默认生成堆分配对象,反序列化一次就触发多次 new;而 flatbuffers 只需一块连续内存(uint8_t*),GetRoot<mymsg>(buf)</mymsg> 直接返回结构体视图,无解析开销。
但要注意:flatbuffers 不支持默认值省略、不支持运行时 schema 变更、不支持嵌套过深(>64 层编译报错)。中小规模 MMORPG 或实时对战服足够用,但带复杂配置热更的沙盒类游戏建议保留 protobuf + 自研 arena allocator。
- 生成代码必须加
-g(生成 C++17constexpr访问器),否则性能损失 15%+ - 客户端和服务端必须用同一份
.fbs文件生成,禁止手工改字段顺序——flatbuffers靠 offset 定位字段,顺序错就直接读垃圾内存 - 二进制协议头建议自己加 4 字节 magic(如
0x46425300)和 2 字节版本号,便于未来灰度升级
怎么避免 std::shared_ptr 导致的 GC 式卡顿?
游戏服最怕偶发 50ms+ 的停顿,而 shared_ptr 的引用计数原子操作在高并发下会引发 cache line bouncing,尤其在连接断开、玩家登出这种批量销毁场景。实测 10k 连接同时下线,纯 shared_ptr 管理 session 可导致主线程卡顿 80ms 以上。
解决方案不是不用智能指针,而是分层控制所有权:
- 连接生命周期由
io_context独占管理(用std::unique_ptr<session></session>存于std::vector或tsl::robin_map中) - 业务逻辑中需要跨协程传递的临时引用,用
session*+weak_ptr校验(注意:校验后必须立即转回shared_ptr再用,否则仍可能 dangling) - 所有定时器(如心跳、技能 CD)统一走一个
std::vector<timer_entry></timer_entry>+ 时间轮(std::array<:list>, 256></:list>),避免为每个 timer 创建独立shared_ptr
Linux 下 epoll 边缘触发(ET)模式为什么比水平触发(LT)更稳?
LT 模式下,只要 socket 缓冲区有数据,epoll_wait 就持续返回该 fd,容易掩盖「没读完」的 bug;而 ET 模式只通知一次,强制你必须循环 recv 到 EAGAIN,天然防粘包遗漏。游戏服消息粒度细(一个 TCP 包可能含 3 个协议包),ET 是底线要求。
但 asio 默认封装的是 LT 行为(epoll 底层虽支持 ET,但 asio 为兼容性屏蔽了它)。要启用 ET,必须绕过 asio 的 socket 封装,自己调用 epoll_ctl 并设 EPOLLET,再用 asio::posix::stream_descriptor 包裹原始 fd —— 这意味着你得自己写 read loop 和 error dispatch。
- ET 下
recv必须用MSG_DONTWAIT,否则阻塞住整个事件循环 - 务必检查
recv返回值:=0 是对端关闭,errno == EAGAIN || errno == EWOULDBLOCK 才算读完,其他错误(如ECONNRESET)要立即 close - 不要在 ET 模式下混用
send和write,统一用send并检查MSG_NOSIGNAL
真正难的不是写出来,是压测时发现某个边缘连接在 99.99% 场景下正常,但特定丢包序列下会漏掉最后一个 EPOLLIN 事件——这时候 ET 的严格性反而成了救命稻草,因为问题会立刻暴露,而不是藏在内存泄漏或状态不一致里拖到凌晨三点。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











