基于 C++、Linux epoll、TCP、Protobuf、网格 AOI 与 timerfd 时间轮设计的轻量级 MMO 游戏服务器学习项目。
| 项目 | 说明 |
|---|---|
| 开发语言 | C++ |
| 运行平台 | Linux |
| 网络模型 | 单线程 epoll LT 事件循环 |
| 通信协议 | TCP + 自定义长度帧 + Protobuf |
| 视野管理 | 网格九宫格 AOI |
| 定时机制 | timerfd + 时间轮 |
| 当前状态 | 架构与设计文档公开,源码独立重写中 |
项目围绕三个问题展开:
- 如何分离网络收发、协议解析和游戏业务;
- 如何在 TCP 字节流上识别完整的游戏消息;
- 如何避免玩家移动消息无差别广播给全服玩家。
服务器采用“通道层—协议层—业务层”的职责划分,并由 epoll 统一监控网络和定时事件。
flowchart LR
C["游戏客户端"] -->|"TCP 字节流"| CH["通道层"]
CH -->|"原始字节"| P["协议层"]
P -->|"Protobuf 业务消息"| G["业务层"]
G --> A["AOI 视野管理"]
G --> W["玩家与世界状态"]
T["timerfd"] --> E["epoll 事件循环"]
E --> CH
E --> TW["时间轮调度"]
G -->|"响应消息"| P
P -->|"完整协议帧"| CH
CH -->|"TCP"| C
- 管理监听套接字和客户端连接;
- 处理
accept、recv、send与连接关闭; - 将收到的原始字节交给协议层;
- 不包含具体游戏规则。
- 从 TCP 字节流中提取完整的应用层消息;
- 根据消息类型创建对应的 Protobuf 对象;
- 完成序列化、反序列化、组帧和解帧;
- 在业务消息与网络字节之间建立边界。
- 处理玩家上线、下线、移动和世界聊天;
- 管理玩家 ID、昵称、坐标与场景状态;
- 根据 AOI 查询结果决定消息接收者;
- 通过协议对象发送业务响应,不直接调用 socket API。
Linux 中,监听套接字、客户端连接和 timerfd 都可以通过文件描述符表示。服务器将这些文件描述符注册到同一个 epoll 实例,由一个事件循环统一分发:
监听套接字可读 -> accept 新连接
客户端连接可读 -> recv 玩家消息
客户端连接可写 -> 发送缓冲区数据
timerfd 可读 -> 推进时间轮
epoll 只负责通知“哪些文件描述符已经就绪”,实际的连接建立、数据收发和业务处理仍由服务器完成。
TCP 是连续字节流,不保存应用层消息边界。项目使用固定 8 字节消息头:
+----------------------+----------------------+-------------------+
| Payload Length (4B) | Message Type (4B) | Protobuf Payload |
+----------------------+----------------------+-------------------+
每个连接维护独立接收缓冲区:
- 新数据先追加到缓冲区;
- 不足 8 字节时等待下一次接收;
- 读取消息体长度与消息类型;
- 消息体未收完整时保留现有数据;
- 收到完整消息后取出并反序列化;
- 缓冲区仍有数据时继续解析下一帧。
该过程同时覆盖:
- 半包:一条消息分多次到达;
- 粘包:多条消息在一次读取中到达;
- 连续帧:解析一帧后继续消费剩余字节。
Protobuf 负责“业务对象与二进制消息体之间的转换”,长度帧负责“确定一条消息从哪里开始、到哪里结束”,两者职责不同。
如果每次移动都通知全部 N 名玩家,单次广播需要遍历接近 N 个连接。项目将地图划分为固定网格,以当前格及周围八格组成的九宫格作为兴趣区域。
+-----+-----+-----+
| | | |
+-----+-----+-----+
| | P | | P:当前玩家所在网格
+-----+-----+-----+
| | | |
+-----+-----+-----+
玩家跨格移动时分别计算旧视野和新视野:
旧视野 - 新视野:离开视野的玩家;新视野 - 旧视野:进入视野的玩家;新视野交集:继续接收位置更新的玩家。
因此,移动广播对象数由全服玩家数 N 收敛为邻域玩家数 K。这是广播范围的算法变化,不等同于未经压测的具体性能提升比例。
timerfd 将定时器抽象为文件描述符。定时器到期后,该文件描述符变为可读,因此能够与网络连接一起交给 epoll 管理。
时间轮将任务分配到不同槽位:
目标槽位 = (当前槽位 + 延迟刻度) % 槽位数量
剩余圈数 = 延迟刻度 / 槽位数量
每次 timerfd 到期后推进时间轮,只检查当前槽位中的任务。计划中的独立实现将支持:
- 一次性任务;
- 周期任务;
- 任务取消;
- 空服延迟退出;
- 新玩家接入时取消退出任务。
- 当前设计是单线程事件驱动,不是多线程或多 Reactor;
- epoll 使用 LT 模式,减少边沿触发下漏读事件的实现风险;
- AOI 只减少无关广播对象,不能在没有压测数据时宣称具体提升百分比;
- 正式实现需要继续补充协议长度校验、慢连接保护、部分写处理和异常连接回收。