炸服就是因为某种原因瞬间有大流量涌入服务器,导致服务器逐步变慢卡顿直至瘫痪,无法继续提供服务。
游戏服务端技术栈指南
写给谁:从 PHP / Laravel Web 开发转向游戏服务端开发的独立开发者
核心目标:不是罗列技术名词,而是让你在每一个关键分岔路口都能做出不后悔的决策
0. 这份文档怎么用
转型最大的坑,不是技术难,而是在错误的维度上用力。
很多 PHP 出身的开发者转游戏服务端时,会本能地去找”游戏服务端的 PHP 框架”,花三个月搭出一套 Laravel 目录结构,然后发现游戏的核心难点——确定性、延迟、AOI、状态机——一个都没碰过,倒是把 Web 开发那套 RESTful 分层架构复刻了一遍。
所以这份文档的组织方式是这样的:
| 章节 | 解决什么问题 |
|---|---|
| 第 1 章 | 你的大脑模型需要重建哪些假设 |
| 第 2 章 | 选什么语言——决定未来 3 年的开发体验 |
| 第 3 章 | 帧同步 / 状态同步 / 预测回滚怎么选 |
| 第 4 章 | TCP/UDP/KCP、序列化、消息头怎么设计 |
| 第 5 章 | 网关/逻辑服/公共服/DB 代理怎么切 |
| 第 6 章 | 房间、匹配、战斗、背包、排行、聊天的实现要点 |
| 第 7 章 | Redis / MySQL / 分库分表 / 存盘策略 |
| 第 8 章 | Unity / Cocos、netcode 框架选型 |
| 第 9 章 | 策划表怎么进代码库 |
| 第 10 章 | 部署、监控、日志、压测、故障排查 |
| 第 11 章 | 客户端不可信意味着什么 |
| 第 12 章 | 12 周从零到上线一款游戏的路线 |
| 第 13 章 | 直接抄的三套技术栈 |
| 第 14 章 | 书、仓库、文档、课程 |
| 第 15 章 | 别人踩过、你不必再踩的 40 个坑 |
| 附录 A | 你的技能有多少能直接用 |
| 附录 B | 一页纸速查表 |
贯穿全文的判断依据:你是独立开发者、单兵作战、PHP 背景、前端能力偏弱、但有 Laravel/Filament 实战经验(这意味着你懂配置化后台、懂权限模型、懂数据表设计——这在游戏服务端的价值被严重低估了,后面会专门讲)。
读法建议:
- 时间有限 → 读第 1、2、3 章 + 附录 B(半天,足够做出所有关键决策)
- 准备动手 → 全读一遍,然后按第 12 章的路线图走
- 只想抄技术栈 → 直接跳第 13 章
第 1 章 思维迁移:Web 后端 vs 游戏服务端的 7 个本质差异
这一章是全文的地基。如果你直接跳到语言选型,会在三个月后撞墙。
1.1 七个差异速览
| # | 维度 | Web 后端(Laravel) | 游戏服务端 |
|---|---|---|---|
| 1 | 交互模型 | 请求-响应,无状态,HTTP 天然 | 长连接,有状态,玩家状态活在内存里 |
| 2 | 延迟目标 | 200ms 可接受,1s 也能忍 | 战斗内 50ms 决定手感,100ms 开始”粘” |
| 3 | 时间语义 | 一次请求一个时刻,忽略时间 | 时间是第一公民:tick、帧号、超时、时钟漂移 |
| 4 | 一致性 | 事务 + 强一致数据库 | 内存态 + 异步落库 + 最终一致 |
| 5 | 反作弊 | 客户端输入不可信但影响有限 | 客户端输入直接决定战斗结果 → 必须服务端权威 |
| 6 | 流量形态 | 平滑、可预测、弹性伸缩 | 开服尖峰、活动脉冲、连接数是第一指标 |
| 7 | 失败代价 | 5xx 报错,用户刷新重试 | 掉线 = 玩家资产可能回档(等级/背包/经验) |
1.2 差异一:长连接 + 有状态 = 你的人生观要变
Laravel 里你是” stateless 的信徒”:每个请求进来,加载用户、查库、返回、忘记一切。水平扩容so easy,加机器就行。
游戏服务端完全相反。玩家的等级、背包位置、技能 CD、所在场景,全都活在某个进程的堆内存里。 这意味着:
1 | // ❌ 你在 Laravel 里会这么写(每来一个消息就查一次库) |
在 1000 人同场景时,这个写法会在第一个 tick 里就打满你的连接池——每一个移动包都触发两次数据库 IO。
1 | // ✅ 游戏服务端的做法:内存态 + 脏标记 + 批量落库 |
这就是”热/温/冷”三级存储的雏形,第 7 章会展开。
关键认知转变:你在 Laravel 里学到的”每个请求都从权威数据源读取”这个好习惯,在游戏服务端是性能自杀。游戏服务端追求的是数据常驻内存 + 变更可控落盘。
1.3 差异二:延迟目标
Laravel 里你的 SLA 可能是”接口 P95 < 300ms”。游戏服务端要分层看:
| 场景 | 延迟预算 | 说明 |
|---|---|---|
| 登录 | < 1s | 可接受,用户有心理预期 |
| 匹配/组队 | < 3s | 超时用户会重复点击 |
| 普通操作响应(开背包、聊天) | < 100ms | 超过就有”卡”的感觉 |
| 战斗内操作回显 | < 50ms | 超过就能明显感觉到”操作延迟” |
| PVP 判定往返 | < 100ms(RTT) | 超过就开始”打不到人” |
50ms 的 P99 意味着什么:不是”平均 50ms”,而是 100 次操作里有 1 次超过 50ms。这意味着:
- 你的逻辑处理必须在 1~5ms 量级(剩下的都留给网络往返)
- 你不能有任何 同步阻塞 IO(查库、调 HTTP、写文件)在主逻辑线程上
- 你不能用 GC 停顿几十毫秒的语言在关键路径上(这是 C++/Rust/Go 相对 Java 的核心优势)
1.4 差异三:时间是第一公民
Web 开发里时间是隐形的:请求到达就处理,处理完就返回。
游戏服务端里,每一帧都是一个时间契约:
1 | 60 FPS → 每帧 16.67ms 预算 |
在帧同步体系里,逻辑帧率是协议的一部分,客户端和服务端必须约定一致(第 3 章详述)。
时间相关的坑,一个 Web 开发者几乎必然要踩:
Time.deltaTime在客户端和服务端不一致 → 帧同步必不同步- 时钟漂移:多台机器的
time()不一样 → 用tick 计数而非墙上时间做逻辑基准 - 超时判断的边界抖动:
>=还是>,在 60fps 和 20fps 下行为不同 - 夏令时 / 时区(PHP 老坑):游戏内时间一律用 UTC 或单调递增的 tick
1.5 差异四:最终一致,而不是强一致
Laravel 里你用 DB::transaction() 把 3 张表的写入包成一个原子操作。游戏服务端很少能这么做,因为:
- 玩家内存态和 DB 里的态本来就有时间差(差 30 秒)
- 跨服玩法(跨服务器 PK、跨服聊天、赛季结算)天然跨库
- 排行榜、社交关系这些数据,延迟几百毫秒完全没问题
所以游戏服务端的主流一致性模型是:
1 | 内存态(权威,毫秒级)── 定时/事件 ──▶ Redis(温,秒级)── 批量 ──▶ MySQL(冷,分钟级) |
代价是”回档”。玩家掉线、进程崩溃,可能丢失最后 30 秒的经验。这在 Web 业务里不可接受,在游戏业务里是被行业普遍接受的设计——因为可以用”补偿邮件”、经验双倍掉落活动等方式对冲。
1.6 差异五:反作弊是架构约束,不是补丁
Web 后端通常信任”这个请求来自已登录用户”。游戏服务端必须假设客户端是敌对的:
1 | ❌ 客户端上报「我攻击了敌人,造成 100 伤害」 |
这一条直接决定技术选型:
- 如果你做状态同步(服务端算状态)→ 服务端必须跑游戏逻辑 → 需要高性能语言(Go/C#/C++)
- 如果你做帧同步(客户端算)→ 逻辑在客户端跑,但服务端要重放校验 → 服务端也要跑同一份逻辑 → 同样需要高性能语言,且要求双端共享代码
PHP 在这条路上基本出局(后面展开)。
1.7 差异六:流量形态
Web 后端:日活曲线平滑,峰谷比 3~5 倍,K8s HPA 一缩一放就行。
游戏服务端:
- 开服尖峰:新服开放瞬间,10 倍日常流量挤进来
- 晚高峰脉冲:晚上 8-11 点,社交游戏流量翻 3 倍
- 活动脉冲:限时活动开启,1 分钟内 5 倍
- 连接数是第一指标,不是 QPS。1 万个玩家在线可能只有 200 QPS 的业务请求,但占了 1 万个长连接
这对架构的影响:你的服务必须能优雅地拒绝/降级,而不是崩溃。连接管理、心跳、超时踢出、限流,都是必需组件。
1.8 差异七:失败代价
| 场景 | Web 后端挂了 | 游戏服务端挂了 |
|---|---|---|
| 表现 | 502,用户刷新 | 玩家集体掉线 |
| 数据 | 事务回滚,无损失 | 内存里的玩家数据丢失 |
| 恢复 | 重启即好 | 需要从 Redis/MySQL 恢复玩家状态 |
| 玩家感知 | “网站崩了” | “游戏不玩了” |
所以游戏服务端必须内置状态外置机制(第 5.4、7 章详述)。
1.9 小结:你的 PHP 经验哪些能用,哪些用不了
能直接用(比你以为的多):
| PHP/Laravel 技能 | 在游戏服务端的对应 |
|---|---|
| MySQL 表设计、索引优化 | 玩家/背包/订单表设计,游戏 DB 优化一样 |
| Redis 使用(缓存、排行榜 ZSET、分布式锁) | 游戏服务端 Redis 用法几乎完全一致 |
| Filament / FastAdmin 的配置化后台 | GM 后台、配表管理、线上数据订正——这是独立开发者的救命技能 |
| 权限模型 / RBAC | GM 权限分级(策划看数值、运营改商城、运维看日志) |
| 队列(Redis/Kafka) | 跨服消息、异步落库、邮件下发 |
| 定时任务调度 | 日常任务、赛季结算、离线结算 |
| 限流/防重/幂等 | 支付回调幂等、道具领取幂等 |
| 分布式 ID(雪花号等) | 游戏内全局唯一 ID(timestamp|worldId|sequence) |
| Docker / Supervisor / Nginx | 服务进程管理、反向代理、负载均衡 |
用不了(需要重学):
| Web 习惯 | 游戏服务端为什么不行 |
|---|---|
| 同步阻塞 IO(查库、调 API) | 主逻辑线程会被卡住,所有玩家一起卡 |
| 请求-响应思维 | 玩家状态是持续存在的,不是每个包都从 DB 读 |
| RESTful 路由风格 | 游戏用消息号(opcode)路由,不是 URL |
| JSON 传输 | 高频消息必须二进制(Protobuf/FlatBuffers) |
| 无限扩容假设 | 有状态服不能随便加机器,玩家和进程是绑定的 |
| 协程 = 并发 | 游戏里更常用固定 tick 的事件循环,不是自由并发 |
一句话总结第 1 章:Web 教你的”可扩展性”和”无状态”在游戏服务端大部分时候是反模式;你在 Laravel 里练就的数据建模、后台工具化、配置驱动能力,在游戏服务端则是稀缺技能。
第 2 章 语言选型:这可能是全文最重要的一章
语言选错的代价,是未来 2-3 年的持续摩擦。它决定了:
- 你的 AI 协作者(也就是我)能帮你写多少
- 你招不招得到人(虽然你是单人)
- 性能天花板在哪
- 生态成熟度(工具链、库、踩坑资料)
- 你能不能同时写客户端和服务端(双端共享逻辑)
2.1 六种语言的横向对比
| 维度 | PHP (Swoole/Workerman) | Node.js / TS | Go | C# / .NET | C++ | Rust |
|---|---|---|---|---|---|---|
| 性能上限 | 中 | 中高 | 高 | 高 | 极高 | 极高 |
| 并发模型 | 协程(需扩展) | 单线程事件循环 | goroutine | async/await + TPL | 线程池/协程 | async (tokio) |
| GC 影响 | 小(引用计数) | 大(会卡顿) | 小(STW 极短) | 大 | 无 | 无 |
| 冷启动 | 中 | 快 | 快 | 中 | 慢 | 慢 |
| 编译期检查 | 弱 | 中(TS) | 强 | 强 | 强 | 强 |
| 学习曲线(对 PHP 开发者) | 极低 | 低 | 中 | 中 | 高 | 高 |
| 官方游戏框架 | 无 | Pomelo(停更)/Colyseus | Pitaya/Leaf/Nakama | ET | skynet | 无 |
| 双端共享代码 | ✗ | ✗(H5 除外) | ✗ | ✓ (ET/Unity) | ✓ (cocos) | ✗ |
| 生态成熟度 | Web 极强 / 游戏弱 | Web 强 / 游戏中 | 游戏中上 | 游戏中上 | 游戏最强 | 游戏弱 |
| 招聘/协作 | 容易 | 容易 | 中 | 中 | 难 | 难 |
| AI 辅助产出质量 | 高 | 高 | 高 | 高 | 中 | 中 |
2.2 逐个拆解
PHP 路线:Swoole / Workerman / Laravel Octane
能做什么:
- 长连接 WebSocket 服务器(Workerman / Laravel Reverb)
- 卡牌、棋牌、休闲游戏的大厅 + 战斗服
- H5 小游戏(浏览器唯一可选 WebSocket)
- 你的老本行——游戏后台、GM 系统、运营平台
硬伤:
- 协程需要 C 扩展(Swoole/OpenSwoole)。Windows 本地 Laragon 环境装起来是各种折腾,跨平台开发体验割裂。
- 协程上下文让传统调试工具失效:Xdebug 穿不透协程栈,
var_dump()可能输出空或错乱值。你在 Laravel 里那套”打日志、看堆栈”的排障习惯会直接失灵。 - Swoole BASE 模式下单个协程崩溃可能拖垮整个 Worker;Workerman 则是纯 PHP 多进程,性能上限低但调试体验好。
- PHP 生态里几乎没有游戏服务端框架。你得自己从零实现 Actor、tick 循环、房间管理、协议编解码。PHP 大佬社区里做游戏服务端的项目,质量和文档都不成体系。
- 无法双端共享逻辑。如果你用 Unity/Cocos 客户端,PHP 服务端和客户端代码要写两遍。
什么时候 PHP 是对的选择:
- 你做卡牌 / 棋牌 / 回合制 / 休闲游戏(低实时性要求)
- 客户端是 H5 / 小游戏(WebSocket)
- 游戏是你的老本行的延伸(如你已经熟悉快手开放平台对接,再做一个游戏)
- 团队/客户强制要求 PHP
我的判断:如果你的目标是”做出一个能上线运营的游戏”,PHP + Swoole 可行但不优雅。如果目标是”建立可以持续精进的游戏服务端能力”,PHP 是最差的选择——因为它让你停留在舒适区,不接触真正的技术难点。
Node.js / TypeScript 路线
能做什么:
- H5 / 小游戏全栈(客户端和服务端同语言,收益巨大)
Colyseus:MIT 协议的现代多人游戏框架,房间即核心抽象,schema 声明式状态同步(二进制 patch,默认 50ms/20fps),内置匹配,SDK 覆盖 Unity/Godot/Cocos/Flutter/Swift/原生 CPomelo(网易,2019 停更):设计理念经典但别新项目用- 你已经有 Node.js 服务端运维经验(pm2、进程守护、日志)——这是真实的迁移优势
硬伤:
- 单线程 + V8 GC。Node 的 GC 停顿在 10-100ms 量级,对”50ms 延迟预算”的战斗场景是硬伤。虽然 V8 的分代 GC 和增量标记已经改善很多,但在 tick 循环里做大量对象分配仍会造成周期性卡顿。
- CPU 密集逻辑要靠 worker_threads 逃逸,复杂度陡增。
- 类型安全靠 TS 编译期,运行时仍是 JS。
什么时候 Node 是对的选择:
- H5 / 小游戏 / 微信小游戏
- 团队是前端背景,想快速出原型验证玩法
- 游戏本体逻辑轻(回合制、卡牌、休闲),实时性要求不高
Go 路线 ⭐ 独立开发者的甜点区
能做什么:
Pitaya:Go 写的分布式游戏服务器框架,connector(接入)/ handler(业务)分离,内建 RPC、服务发现(etcd/Consul)、原生 Prometheus/OpenTracing 集成、官方 Dockerfile + K8s Helm ChartLeaf:轻量,单进程内按需组合模块,自带时序调度器Nakama:Go 写的全栈游戏后端——认证、存储、匹配、聊天、排行榜、权威对局循环,脚本可用 Lua/TS/GoOpen Match 2:Google 的匹配服务,gRPC 架构,frontend/backend/director 三件套Agones:K8s 上的专用游戏服务器编排(GameServer/Fleet 资源,本地 SDK sidecar 上报健康,Fleet 保持热备池避免冷启动)KCP(skynet作者的 Go 移植)/gNet(高性能非阻塞网络库)
优点:
- goroutine + channel 天生长连接,百万连接不是梦
- GC 停顿极短(STW 通常 < 1ms),能满足战斗场景
- 编译期类型检查,重构安全——这点对 AI 辅助开发极其重要
- 协程写法让业务代码像同步代码一样直白,接近 PHP 的开发体验
- 二进制部署,单机文件几个 MB,容器化天然适配
- 官方维护的云原生生态(K8s、gRPC、Prometheus、etcd 全家桶)
- AI 训练数据充足,我(以及任何 AI 助手)帮你写 Go 游戏服务端的质量最高
缺点:
- 没有 C++ 的极限性能——但对你单机 1-5 万在线的游戏,Go 绰绰有余。信通院白皮书说 C++ 能效比是 Go 的 1.8 倍,但那是 5000+ 同屏战斗的极端场景。
- 游戏领域的现成框架比 C++ 少(但有 Pitaya/Leaf/Nakama 补上)
- 不能双端共享逻辑(如果你用 Unity 客户端)
什么时候 Go 是对的选择:
- MMORPG / SLG / 卡牌 / 棋牌 / 休闲竞技 等绝大多数商业手游品类
- 想要”单体起步、架构可演进”——从单进程写到分布式集群,Go 的路径最顺
- 想要 K8s 编排 + 弹性伸缩
我的判断:对独立 PHP 开发者,Go 是默认推荐。理由:性能足够、并发模型天然匹配、类型安全让你(和 AI)能重构、生态齐全、招聘容易、我的产出质量最高。缺点只有一个——不能和 Unity 客户端共享代码,但可以通过”协议文件 + 导表”来管理一致性。
C# / .NET 路线 ⭐ 如果你用 Unity,必看
ET Framework 是这个方向的皇冠,功能覆盖度极高:
| ET 能力 | 说明 |
|---|---|
| N 变 1 | 全部服务端组件挂到一个进程,用 VS 单步调试分布式逻辑。这是 ET 最大的杀手锏——分布式服务最难的是调试,ET 直接把它降维成单体调试 |
| 1 变 N | 开发时单进程,发布时改一份配置拆成多进程。改哪部分功能放哪个服,代码几乎不用动 |
| 双端共享逻辑 | 客户端(Unity/C#)和服务端(.NET)同一份 C# 代码。战斗公式、 buff 逻辑写一遍,天然不会不一致 |
| 位置透明 Actor | 拿到 EntityId 就能发消息,不用关心对象在哪个进程。类 Erlang 消息机制 |
| async/await 原生协程 | await session.Call(...) 这种写法在 PHP 里只能靠 Swoole 硬凑 |
| 纯 C# KCP 库 | 作者自研,性能强 |
| 零 GC 二进制序列化 | 基于 BSON,避开 GC 压力 |
| AOI | 九宫格实现,动态调整可见玩家以降低服务器负载 |
| 热更 | 逻辑代码热重载,二进制动态库注入,零停机升级 |
| Recast 寻路 | 服务端 3D NavMesh |
| 压测机器人 | 直接用客户端逻辑层代码写机器人,4 台 24 核机器模拟 1 万人 |
缺点:
- Windows 上开发、Linux 上运行(虽然 .NET 已跨平台)
- C# 的 GC 依然存在(ET 用零 GC 序列化 + 对象池缓解)
- ET 生态的文档以中文为主,国际化文档少
- 如果客户端不是 Unity/Cocos,这套就浪费了
什么时候 C# 是对的选择:
- 客户端是 Unity(尤其是中小规模、逻辑双端共享的 MMO/ARPG)
- 想要双端一份代码(这是巨大的效率提升,能省掉 30-50% 的逻辑开发量)
- 想用 ET 这类成熟框架,不想从零造分布式轮子
我的判断:如果你的客户端是 Unity 且游戏逻辑偏重度(ARPG/MMO/SLG),ET 是最好的选择,双端共享代码的价值无可替代。如果客户端是 Cocos Creator(TypeScript),Go + 协议双导是更自然的组合。
C++ 路线
能做什么:
skynet(云风):中文游戏服务端圈的事实标准,C + Lua 混合。Lua 写业务(策划能改),C 写调度(性能)。Actor 模型,单节点数万连接。云风本人发明的十字链表 AOI 算法就出自这里。- 性能天花板最高,适合 5000+ 同屏战斗、3A 级项目
- 腾讯全异步 Actor 框架单机 10 万连接;网易《逆水寒》用 C++ 协程把 IO 线程数减少 70%
缺点(对独立开发者是致命的):
- 学习曲线陡。C++ 的内存管理、UB、生命周期、游戏服务端特有的指针问题,会让你把大量精力花在跟业务无关的地方
- 编译慢,迭代体验差(skynet 增量编译能缓解,但依然不如 Go/Node 的秒级编译)
- Lua 双向边界有跨语言调用开销,调试链路长
- 生态偏运维传统:没有配置中心、没有 K8s Helm Chart、服务发现要手写
- AI 辅助写 C++ 游戏服务端的质量明显低于 Go/C#
什么时候 C++ 是对的选择:
- 你要做的游戏同屏人数极高(千人国战、5000+ 战斗)
- 你已经会 C++,或者有 C++ 背景的合伙人
- 你要读 skynet / 网易 / 腾讯的源码来学习
我的判断:不推荐你从 C++ 开始。你的时间是最大的成本,Go 能达到 90% 的性能,但开发效率是 C++ 的 2-3 倍。等你真的撞到性能天花板,再考虑把热点模块用 C++ 重写(这也是 Go 生态的常规做法)。
Rust 路线
性能天花板与 C++ 相当,零 GC,内存安全。tokio 是异步运行时的事实标准。
缺点:游戏领域几乎没有成熟框架和资料;学习曲线陡;薪资溢价 40% 说明稀缺度高(也说明生态薄)。不推荐作为转型首选,但如果你想投资未来的技术护城河,Rust 是好选择。
Java / Kotlin 路线
Netty 的 NIO Reactor 模型成熟稳定,国内页游/手游早期爆款几乎全是 Java 服务端,积累了大量线上排障经验。Spring Cloud 生态完整。
缺点:GC 停顿在帧同步类玩法是硬伤(虽然可以调优,ZGC/Shenandoah 已改善很多);JVM 内存占用大,容器密度低;协程需要 Kotlin 或 虚拟线程(Loom,目前仍在演进)。
什么时候 Java 是对的选择:你已有 Java 经验、或者项目需要 Spring 生态全家桶、或者团队是 Java 背景。你没有 → 跳过。
2.3 决策树
1 | 你的客户端引擎是什么? |
2.4 如果你坚持 PHP(我把话说明白)
不要用 Laravel 硬改。Laravel 的请求-响应生命周期和游戏的常驻内存模型是冲突的。
如果一定要 PHP,用这个组合:
| 层 | 技术 | 说明 |
|---|---|---|
| 长连接网关 | Workerman 或 Swoole/OpenSwoole | Workerman 调试友好(Xdebug 可用),Swoole 性能高但调试困难 |
| 业务后台 | Laravel + Octane(Swoole/FrankenPHP/RoadRunner) | 你最熟悉的部分,GM/运营/配表后台 |
| 通信 | WebSocket(Reverb 或 Workerman) | H5 唯一选择 |
| 序列化 | Protobuf(不要 JSON) | PHP 的 protobuf 扩展有坑,可考虑手写 varint 编解码或用 msgpack |
| 状态 | Redis | 会话、背包缓存、排行榜 ZSET |
| 战斗逻辑 | 独立进程跑(Node/Go) | 把重逻辑剥离出去,PHP 只做大厅 |
这是”混合架构”:PHP 做它擅长的(后台、运营、H5 接入),Go/Node 做它擅长的(实时战斗)。你会看到大厂也是这个路子——腾讯《和平精英》匹配系统用 Go 重构后响应速度提升 40%;网易《逆水寒》C++ 核心 + Lua 脚本扩展。
2.5 我的最终推荐
主推:Go + (Unity 用 NGO/FishNet 或 ET / Cocos 用 Protobuf 协议) + Redis + MySQL + Protobuf + KCP/TCP + Docker
理由,按权重排序:
- 性能足够:Go 的性能对绝大多数商业游戏绰绰有余,GC 停顿 < 1ms
- 开发效率接近 PHP:goroutine 让业务代码像同步代码一样写,
go func()就完事 - 类型安全:struct + 强类型 + 编译期检查 → 你(和 AI)重构时不会炸。这是长期开发体验的关键
- AI 辅助质量最高:Go 是 LLM 训练数据最丰富的后端语言之一,我给你写出来的代码可靠性最高
- 云原生原生适配:容器化、K8s、Prometheus 全是现成的
- 生态有成熟框架:Pitaya / Leaf / Nakama / Open Match / Agones
- 单兵可行:一个人能维护的范围
如果你的客户端是 Unity 且要做重度逻辑 → 改推 ET (C#),双端共享代码能省掉巨量工作量。
如果你的客户端是 H5/小游戏 → 改推 Node.js + Colyseus。
2.6 一张选型速查表(可打印)
1 | 场景 → 首选 备选 |
第 3 章 同步模型:游戏服务端的第一性问题
这是整份技术栈文档里技术含量最高、也最容易被 Web 开发者误解的一章。
如果你只读一章,读这章。
3.1 先纠正一个 Web 直觉
在 Web 里,「数据同步」通常指:前端发请求 → 后端返回最新数据 → 前端渲染。一次一请求,一次一真相。
游戏里完全不同。一个玩家在跑图,他每秒钟会产生几十次状态变化(位置、朝向、动画状态、技能特效、附近实体的相对位置)。你要决定的是:
这几十次变化里,哪些走网络、谁算、什么时候算、算错了怎么办。
这一章的三种答案,对应三种完全不同的技术栈。
3.2 三大同步模型全景对比
| 维度 | 状态同步 (State Sync) | 帧同步 (Lockstep) | 预测回滚 (Prediction + Rollback) |
|---|---|---|---|
| 网络传什么 | 游戏对象状态 | 玩家操作指令 | 指令 + 权威状态 |
| 谁在算逻辑 | 服务器 | 所有客户端 | 客户端算(预测)+ 服务器算(权威) |
| 计算分布 | 100% 服务端 | 100% 客户端(服务器只中转/校验) | 双端都算 |
| 带宽 | 高(状态数据多) | 低(只传输入) | 中 |
| 延迟体感 | 差(RTT 全额延迟) | 中(锁步等待) | 好(本地零延迟) |
| 反作弊 | 强 | 弱(需重放校验) | 强 |
| 确定性要求 | 无 | 极高 | 高(仅重放部分需确定) |
| 开发难度 | 中 | 高 | 高 |
| 典型品类 | MMORPG、卡牌、SLG、棋牌 | RTS、MOBA、格斗 | FPS、TPS、竞速、动作 |
| 服务器压力 | 高 | 低 | 中 |
| 适合独立开发者 | ✅ 最适合 | ❌ 坑极多 | ⚠️ 有挑战 |
3.3 状态同步(State Synchronization)
原理
服务器持有权威状态,玩家客户端只发”我要做什么”,服务器算完把结果广播出去。
1 | 客户端 服务器 |
关键子技术:客户端插值(Interpolation)
服务器不可能 60Hz 给每个玩家发位置(带宽爆炸)。实际做法:
- 服务器以 10~20Hz 发送状态快照
- 客户端收到后不立即显示,而是放到一个缓冲区
- 客户端渲染时,播放 比最新快照落后约 100ms 的历史状态
这就是为什么状态同步的网络游戏里,人物移动看起来”稍微延迟一点点但很顺滑”。用 100ms 的延迟换来了 6 倍的带宽节省。
1 | // 客户端插值缓冲的基本结构(伪代码) |
插值的代价:任何被插值的实体都会”落后”。所以本地玩家不应该用插值,应该用预测(第 3.5 节)。
状态同步的带宽控制
一个 100 人同场场景,假设每个实体 20 字节状态,20Hz 更新:
- 全量广播:100 × 100 × 20 × 20 = 4 MB/s —— 荒谬
- 实际做法:AOI 裁剪(只发视野内实体,见第 6.3 节)+ 增量更新(只发变化的字段)+ 降频(远处低频)
裁剪后,视野内通常只有 20~30 人:
- 30 × 30 × 20 × 20 = 360 KB/s —— 仍然不小
- 加上增量(只发位置变化,实测约 8 字节)和降频(远处 5Hz),可以压到 50 KB/s 以内
带宽预算的实用经验值:单个在线玩家的下行带宽控制在 10~30 KB/s 是健康区间。1 万玩家 × 20KB/s = 200 MB/s ≈ 1.6 Gbps 出口——这是选机器时的硬指标。
状态同步的三个隐藏成本
- 服务器必须跑完整游戏逻辑 → 性能压力全在服务端 → 直接排除 PHP
- AOI 是必需品,否则带宽直接爆
- 客户端必须处理”服务器说你在哪,你在移动”的不一致 → 需要预测或硬直
3.4 帧同步(Lockstep / Deterministic Lockstep)
原理
服务器只同步输入,每个客户端自己算。
1 | 时间轴: |
致命前提:确定性(Determinism)
同样的初始状态 + 同样的输入序列,在任何机器上、运行任何多次,必须得到逐比特一致的结果。
这是帧同步的全部难点。Unity 环境下有一长串破坏确定性的东西:
| 破坏源 | 说明 | 解决方案 |
|---|---|---|
| 浮点数误差 | 不同 CPU 架构(Intel vs AMD)、不同编译器优化等级、FMA 指令有无,会产生微小差异 | 核心逻辑用定点数(Fixed Point) |
Time.deltaTime |
帧率不同 → delta 不同 → 逻辑分叉 | 固定逻辑帧率(如 20 FPS),用 1.0f/20.0f 常量 |
Random 无固定种子 |
每次运行结果不同 | 自定义确定性 PRNG(如 xorshift),服务端开局下发种子 |
| 容器遍历顺序 | Dictionary / HashSet 遍历顺序不保证 |
用有序容器(List + 显式排序,或 SortedDictionary) |
| 多线程调度顺序 | 协程/线程执行顺序不固定 | 逻辑单线程,或严格同步点 |
| Unity 物理引擎 | Physics.Simulate 内部实现不确定 |
不用 Unity 物理做核心逻辑,自己写简单 AABB / 圆形碰撞 |
| 对象创建顺序 | 不同端创建顺序不同 → 实体 ID 分配不同 | 确定性 ID 分配(种子 + 计数器) |
| 网络输入到达顺序 | 包乱序 | 用帧号排序,等齐再执行 |
关键设计原则:逻辑层与表现层彻底分离。
1 | ┌─────────────────────────────────────────────┐ |
Unity 项目里做严格帧同步,专业说法是:Transform、Rigidbody、Animator、Particle 属于表现层;核心战斗逻辑用定点数算位置、技能、碰撞、伤害,物理只做表现或非核心辅助。
帧同步的延迟问题与回滚
纯锁步有个致命问题:必须等最慢的人。
如果网络最差的玩家 RTT 是 200ms,一帧 50ms(20FPS),那大家都要等 4 帧才能推进 everyone。实际操作上会”卡帧等输入”。
解决方案是 预测 + 回滚(Prediction + Rollback),也就是 GGPO 的思路:
1 | 1. 玩家按方向键 |
实现回滚的四个难点:
- 状态快照:每帧保存状态
- 手动实现(项目早期方便,但繁琐)
- 反射(性能有问题)
- 代码生成(推荐):只适用内存紧凑型游戏,可用
memcpy - 命令模式(Command Pattern):适用大部分游戏,因为游戏内存通常很复杂——不保存内存,而是记录”我做了什么操作”,回滚时反向撤销
- 回滚后表现层的处理:生成的 mesh、特效要销毁重建
- 回滚后的日志混乱:拆分多份日志,或加实例 ID 区分
- 重演必须是纯函数:不能有外部 IO、不能有随机数副作用、不能有发奖/发邮件这类一次性副作用
1 | // 典型的 Replicate / Reconcile 模式(Unity + FishNet 风格) |
客户端预测 + 服务端校验(Reconciliation)是现代动作游戏的标配。 FishNet 内置这套机制(这是很多人从 Mirror 迁走的主要原因),Mirror 的通用预测能力官方仍标注为 “Researching”。
帧同步 vs 状态同步,怎么选
| 判断维度 | 选帧同步 | 选状态同步 |
|---|---|---|
| 玩家数 | ≤ 8(通常) | 几十到上万 |
| 强反作弊要求 | 低(需重放校验) | 高 |
| 物理主导 | 是(但要用确定性物理) | 否 |
| 带宽紧张 | ✅ 帧同步带宽极低 | ❌ 状态同步带宽大 |
| 服务器成本敏感 | ✅ 服务器压力小 | ❌ 服务器扛全部逻辑 |
| 团队规模 | 小且强 | 任意 |
| 品类 | RTS / MOBA / 格斗 | MMORPG / 卡牌 / 棋牌 / SLG |
对独立开发者的直接建议:
- 除非你做的是 RTS/MOBA/格斗,且人数 ≤ 8,否则选状态同步。
- 帧同步的确定性工程量(定点数库、状态快照/回滚、确定性物理、双端一致的环境)通常是3-6 个月的额外工作,而且极易做出”99% 情况一致,1% 情况永久 desync”的产品——这种 bug 排查起来是噩梦。
- 状态同步虽然费带宽、费 CPU,但问题域是线性可理解的,更适合一个人维护。
3.5 预测回滚(Prediction + Reconciliation)—— 动作游戏的标准解
这是 3.2 表格 里第三种,实际它是状态同步 + 客户端预测的混合,也是目前 FPS/TPS/竞速/动作游戏的事实标准。
核心三件套:
| 技术 | 作用 | 归属 |
|---|---|---|
| 客户端预测 (Client-side Prediction) | 本地输入立即生效,零延迟手感 | 客户端 |
| 服务器权威 (Server Authority) | 服务器算权威结果,校验合法性 | 服务端 |
| 客户端回滚重演 (Reconciliation) | 收到权威状态后比对,不一致则回滚重演 | 客户端 |
| 服务器延迟补偿 (Lag Compensation) | 服务器根据客户端的历史视角重新计算命中判定 | 服务端 |
| 实体插值 (Entity Interpolation) | 远端玩家用插值渲染,不回滚 | 客户端 |
延迟补偿(Lag Compensation)是服务器权威架构里最容易被低估的部分。
问题:玩家 A 看到 B 在屏幕左侧,开了枪。但在网络传输 + 服务器处理期间,B 已经跑到了右边。按服务器当下的状态,这一枪应该打空。但玩家明明瞄准了。
解法:服务器保存每个玩家最近 N 帧的历史位置。当收到 A 的射击请求时,服务器根据 A 的”客户端视角时间”(通常是 A 收到这一帧的往返时间的一半之前)回溯到那个时刻的 B 的位置再判定。
1 | // 延迟补偿的核心逻辑(简化) |
注意这里客户端也必须保存”我开枪时的历史位置”,否则回溯后射线起点不对。这需要客户端在每个 tick 记录自己的位置。
这是”服务器权威”架构的必然复杂度来源。PHP 做不了这个——你需要每 tick 高频写历史位置、结构紧凑的环形缓冲、低延迟的状态共享。Go/C#/C++ 都能做。
3.6 如何选:一张决策流程图
1 | 你的游戏是什么品类? |
3.7 同步相关的调试工具(务必自己造)
这三个工具是每个做同步的游戏都必须有的,从第一天就要有:
| 工具 | 作用 | 实现难度 |
|---|---|---|
| 状态校验和(Checksum) | 每 N 帧计算全局状态 hash,客户端间/客户端-服务端比对,不一致立即报警 | 低(关键!) |
| 网络模拟器 | 在本地注入延迟(50/100/200ms)、丢包(1%/5%)、抖动、乱序 | 中(所有 netcode 框架都自带) |
| 回放系统(Replay) | 记录输入流,可离线重放整个对局 | 中 |
1 | // 状态校验和:帧同步项目的救命稻草 |
每个做帧同步的人都应该记住:没有 checksum 的帧同步项目,第一次 desync 出现时你无从下手,整个项目就此卡死。
3.8 同步模型 → 技术栈的映射
| 同步模型 | 服务端必须具备 | 语言建议 | 框架建议 |
|---|---|---|---|
| 状态同步 | 跑完整游戏逻辑、每 tick 处理 | Go / C# / C++(PHP 出局) | 自研 + Pitaya/Leaf 骨架 |
| 帧同步 | 跑同一份逻辑(双端共享)、状态快照 | C#(ET 双端共享最佳) 或 C++ | ET / 自研 + GGPO 思路 |
| 预测回滚 | 历史位置环形缓冲、低延迟 | Go / C# | 客户端用 NGO/FishNet/Quantum |
第 4 章 网络层:传输协议与消息设计
4.1 传输协议选型
五种协议的实际表现
| 协议 | 可靠性 | 延迟 | 带宽 | 穿透 | 典型用途 |
|---|---|---|---|---|---|
| TCP | ✅ 有序可靠 | ❌ 队头阻塞,抖动时延迟飙升 | 中 | ⚠️ 需中转 | 登录、聊天、邮件、GM |
| UDP | ❌ 不可靠 | ✅ 最低 | ✅ 最省 | ✅ 最好 | FPS 射击、状态同步 |
| KCP | ✅ 可靠有序 | ✅ 快(牺牲带宽换延迟) | ❌ 费带宽 | ✅ | 游戏行业主流(战斗/移动) |
| WebSocket | ✅ | 中 | 中 | ❌ | H5 / 小游戏 / 后台推送 |
| QUIC | ✅ | ✅ 快 | 中 | ✅ | 移动端网络切换场景(未来趋势) |
各协议的致命细节
TCP 的队头阻塞——这是最容易被低估的问题。
TCP 保证字节流有序到达。如果一个包丢了,后面所有包都要等它重传。在移动网络下(4G 抖动、WiFi 切换),单次 RTT 从 30ms 涨到 300ms 是常态,TCP 上的表现是整条连接的所有消息一起卡住。
游戏行业对此的解法是 KCP(skynet 作者云风的作品):
- 基于 UDP 实现可靠有序传输
- 快速重传(RTO 计算激进,丢了立刻重传而不是等超时)
- 选择性确认(只重传真正丢的包)
- 代价:带宽消耗比 TCP 高 10~30%
1 | KCP 的取舍:牺牲 20% 带宽,换取 50% 的延迟下降。 |
KCP 的三个使用要点(很多人在这里翻车):
- `KCP 的 stream 模式和 message 模式要选对:游戏协议用 message 模式(保留消息边界),不要用 stream 模式自己切包
- 必须自己做心跳:KCP 的底层 UDP 没有连接概念,需要应用层心跳(一般 10~20s 一次,3 次超时就断开)
- 必须自己做重连:KCP 不支持连接迁移,重连要重新握手 + 恢复状态
WebSocket 的场景:浏览器唯一选项。所有 H5 小游戏、微信小游戏只能用 WebSocket(或 WebTransport 试验性)。它的握手开销大(HTTP Upgrade),但后续是二进制帧,性能尚可。
QUIC:基于 UDP 的多路复用,解决 TCP 队头阻塞 + 支持连接迁移(手机从 WiFi 切 4G 不用重连)。生态还在成熟中,新项目可以考虑,但不作为首选。
实战推荐:混合协议
没有一个游戏只用一种协议。 典型配置:
1 | ┌────────────────────────────────────────────┐ |
实战技巧:在同一个端口上同时支持 TCP 和 UDP(很多游戏这么干),或者用协议嗅探:收到首包后判断是 HTTP / WebSocket / KCP,分发给不同处理器。这样运维只需开放一个端口。
4.2 序列化选型
对比
| 方案 | 体积 | 序列化速度 | 反序列化速度 | 零拷贝 | schema 演进 | 跨语言 |
|---|---|---|---|---|---|---|
| JSON | 大 | 慢 | 慢 | ✗ | 无(靠约定) | ✅ 最好 |
| Protobuf | 小 | 快 | 快 | ✗ | ✅ 强 | ✅ 好 |
| FlatBuffers | 中(比 pb 大 ~25%) | 最快 | 最快 | ✅ | ✅ 强 | ✅ 好 |
| MessagePack | 小(比 pb 小 ~40%) | 中 | 中 | ✗ | 中 | ✅ 好 |
| Cap’n Proto | 小 | 极快 | 极快 | ✅ | ✅ 强 | ✅ 好 |
(体积/速度数据来自公开的横评测试,量级参考用,精确值随数据类型变化)
选型结论
| 场景 | 选择 | 理由 |
|---|---|---|
| 通用 RPC / 业务接口 | Protobuf | 综合最优、生态最广、向后兼容、团队成员最熟 |
| 帧同步 / 高频状态推送 | FlatBuffers | 零拷贝读取,帧同步每秒几十次序列化,收益明显 |
| H5 / 调试接口 / 后台 | JSON | 可读性 > 一切,浏览器原生支持 |
| 极限带宽优化 | MessagePack + gzip | 体积最小,适合移动端 |
| 配置表 | JSON / 二进制(自定义) | 见第 9 章 |
2026 年的最佳实践不是”自研二进制协议”,而是”基于成熟序列化库定制消息帧”。自研只保留消息头、加密、校验层,序列化体交给 Protobuf / FlatBuffers 处理。
为什么 Protobuf 是默认选择:
- 向后兼容做得最好(加字段不破坏老客户端)
- 跨语言代码生成成熟(Go / C# / C++ / Java / PHP / Rust / TS 全都有)
- 生态工具链完整(
protoc、grpc、buf) - 团队接手成本低
FlatBuffers 什么时候值得:
- 帧同步里每秒几十次序列化,零拷贝收益可观
- 需要”不反序列化就读取某个字段”(比如只想读 HP 做校验)
- 代价:schema 演进比 Protobuf 麻烦,工具链不如 Protobuf 成熟
4.3 消息帧设计
消息头
游戏协议的消息头通常包含:
1 | ┌──────────────────────────────────────────────────┐ |
固定头 16 字节,用二进制读写(binary.BigEndian / PutUint32),不要用 JSON 包装。
1 | // Go: 消息帧的读写(生产级) |
踩坑提醒:
fr.buf = fr.buf[total:]这种切片写法会一直持有底层数组的引用,长期运行会导致内存持续增长。生产环境应该用bytes.Buffer或定期 copy 剩余数据。Go 里更好的做法是固定大小的 ring buffer。
消息号(Opcode)设计
1 | 0x0001 - 0x00FF 握手/心跳/基础 |
约定:
- 客户端 → 服务端(C2S)和 服务端 → 客户端(S2C)用不同号段或用不同最高位标记
- 保留
0xFFFF做错误/调试指令 - 消息号一旦发布永不复用(老客户端还在线时不能用旧号)
哪些消息可以”不可靠”
并非所有消息都需要重传。设计协议时给每个消息打上可靠性标记:
| 消息类型 | 可靠性 | 说明 |
|---|---|---|
| 位置同步(高频) | 可丢 | 下一帧会覆盖,丢了就丢了 |
| 技能释放 | 必须可靠 | 丢了玩家会放不出技能 |
| 伤害结算 | 必须可靠 | 丢了就少伤害 |
| 聊天消息 | 必须可靠 + ACK | 需要送达确认 |
| 背包变更 | 必须可靠 | 数据不一致 = 资损 |
4.4 加密与防篡改
游戏客户端的请求必须假设全部是伪造的。
| 层次 | 措施 | 说明 |
|---|---|---|
| 传输 | TLS / WSS | 防中间人。注意:TLS 增加握手延迟,KCP 场景通常自己实现握手加密 |
| 协议头 | 校验和 / CRC | 防随机损坏和简单篡改 |
| 请求体 | HMAC 签名(含时间戳 + nonce) | 防重放 + 防篡改 |
| 业务 | 服务端权威校验 | 最重要的一层:所有数值服务端重算 |
1 | // 请求签名防重放 |
但要明白:签名防的是”随手改包”和”脚本小子”,防不住内存修改器。真正的防线是服务端权威——客户端报什么都是无效的,服务端自己算。签名只是过滤掉 99% 的低级攻击,减少带宽和 CPU 浪费。
4.5 心跳与超时
| 参数 | 典型值 | 说明 |
|---|---|---|
| 心跳间隔 | 15~30s | 移动端网络波动大,太短浪费带宽 |
| 心跳超时 | 3 次心跳未响应 | 即 45~90s 判定掉线 |
| 客户端 TCP keepalive | 30s | 操作系统层兜底 |
1 | // Go: 连接生命周期管理 |
重要:不要只依赖心跳包。任何收到数据都算”活跃”,这能避免玩家在挂机(但连接还在)时被误踢。
4.6 断线重连
玩家在电梯里断网 30 秒,游戏必须能接回来。
标准做法:
1 | 1. 客户端保存 session_token(登录时服务端下发,带玩家 ID + 登录时间) |
1 | // 重连时的状态恢复 |
断线保留时长是个产品决策:保留太久浪费内存(内存里每个玩家对象可能几 KB),太短玩家体验差。常见做法是保留 5~10 分钟。
4.7 流量控制与背压
这是 Web 开发者最容易漏掉的一环。Web 里一个慢客户端只会占一个请求线程;游戏里一个慢客户端会拖住整个广播。
1 | // 每个连接一个发送队列 + 上限,超出就断开(而不是无限堆积) |
关键原则:宁可断开一个卡住的客户端,也不要让整个房间的广播被它拖住。
其他流量控制手段:
- 令牌桶限流:单客户端每秒消息数上限(防客户端 bug 刷屏)
- 广播分级:核心战斗消息(血量、死亡)> 位置更新 > 特效表现(可丢)
- 背压传染防护:一个场景服的广播循环里,任何一个连接写失败要立即跳过,不能阻塞其他人
4.8 带宽实测参考
| 品类 | 单玩家下行 | 单玩家上行 | 备注 |
|---|---|---|---|
| MMORPG | 10~30 KB/s | 1~3 KB/s | AOI 裁剪后 |
| 状态同步竞技 | 15~40 KB/s | 2~5 KB/s | 含预测回滚 |
| 帧同步 MOBA | 3~8 KB/s | 1~2 KB/s | 只传输入,优势巨大 |
| 回合制/卡牌 | < 1 KB/s | < 0.5 KB/s | 几乎可以忽略 |
| H5 小游戏 | 5~15 KB/s | 1~2 KB/s |
1 万玩家 × 20KB/s ≈ 200 MB/s ≈ 1.6 Gbps —— 这是选机器时的第一道算术题,比”C++ 还是 Go”重要得多。
第 5 章 服务端架构:从单服到分布式
5.1 演进路径
不要一开始就做分布式。 这是独立开发者最常犯的架构错误——还没跑通玩法就先搭了 K8s 集群。
1 | 阶段 1:单进程单体 ← 从这里开始 |
判断标准:
- 阶段 1 能撑 单服 1000~3000 人(Go + 优化得当)
- 阶段 2 撑 单服 1 万人 / 集群 10 万人
- 阶段 3 是 MMORPG 的常规形态
- 阶段 4+ 只有大公司才需要
独立开发者的现实建议:从阶段 1 开始,把架构设计成”逻辑分层,物理单机“。代码里分好 gateway / logic / common / dbproxy 的包,部署时先跑在同一个进程里,等有需要再拆进程——改配置就能拆,这是 ET 框架的 1 变 N 思路,非常值得借鉴。
5.2 经典分层架构
1 | ┌──────────────────────────────────────────────────────────────┐ |
各层的关键设计
接入层(Gateway)—— 你的”无状态前端服务器”
它的职责边界很清晰:
- 握手、鉴权、限流
- 协议编解码(这是最耗 CPU 的部分之一,和业务逻辑分开可以独立扩容)
- 维护
playerID → conn映射 - 软路由(ET 框架的特性):客户端连的是网关,网关把逻辑请求转发到真正的逻辑服;当逻辑服被攻击/过载时,可以动态换路由,客户端无感知、不断连、不丢数据
1 | // Gateway: 极简的连接管理 |
逻辑层(GameServer)—— 你的”游戏世界”
这是整个系统最重的部分。核心特征:
- 固定 tick 循环(不是事件驱动,也不是自由并发)
- 单线程处理一个场景(避免锁)
- 玩家的所有状态在这里的内存里
1 | // 场景服的 tick 循环 —— 整个游戏服务端的心脏 |
为什么是固定 tick 而不是事件驱动?
- 可预测:每帧的工作量可控,不会因为某个事件突然卡住整帧
- 可同步:如果要做帧同步,固定 tick 是前提
- 易调试:问题可以按 tick 重放
缺点是”响应延迟”最高为一个 tick(20 FPS = 50ms)。但对于状态同步游戏,这个延迟被插值掩盖了。
公共层(CommonService)—— 你的”共享服务”
匹配、排行、聊天、公会这些功能不该每个逻辑服都跑一份。抽出来的好处:
- 排行榜是全局的(分服排行 vs 全服排行的实现完全不同)
- 聊天需要跨场景广播
- 匹配需要跨服搜索玩家
- 独立扩容(活动期间聊天服务可以单独加机器)
数据层(DBProxy)—— 你的”存储代理”
不是简单地”包一层 MySQL 驱动”,而是承担:
- 读写分离:写走主库,读走从库
- 分库分表路由:
playerID % 16→ 决定库表 - 缓存旁路:读请求先查 Redis,miss 再查 MySQL 并回填
- 写缓冲:内存队列攒批,异步刷盘
5.3 有状态 vs 无状态的边界
这是分布式架构里最核心的取舍。边界划错了,扩展能力就废了。
| 层 | 状态性 | 能不能随便加机器 | 为什么 |
|---|---|---|---|
| Gateway | 无状态 | ✅ 随便加 | 状态在客户端连接上,进程崩了连接断了客户端重连即可 |
| 逻辑服 | 有状态 | ❌ 需迁移 | 玩家对象在内存里 |
| 公共层 | 无状态 | ✅ 随便加 | 无状态或状态在 DB |
| DBProxy | 有状态但单一职责 | ⚠️ 谨慎 | 连接池是状态,但可以分片水平扩 |
关键认知:”有状态”不等于”不能扩展”。有状态服可以通过一致性哈希分片来扩展:
1 | // 一致性哈希路由:玩家 → 节点 |
1 | 取模路由:玩家在 node = id % 3 |
5.4 状态外置与故障恢复
有状态服最怕崩溃——进程一挂,内存里 2000 个玩家对象全没了。
三级存储方案:
1 | 热数据:进程内存(Player Object) 访问:纳秒级 丢失:崩溃即丢 |
1 | // 定时快照:每 30 秒把脏数据刷到 Redis |
更进一步的保障:
- 玩家数据在 Redis 里的 TTL 应该是 1 小时,但定时快照每 30 秒写一次 → 最多丢 30 秒
- 重要数据(充值、付费道具)走独立链路,实时落库 + 幂等设计
- 战斗中的玩家不落盘(副本/竞技场是临时状态,结束后结算一次)
5.5 分服与跨服
分服(Shard / World)
早期页游和现在的手游都用分服。核心是数据隔离:
1 | 方案 A:database-per-world(最常见) |
账号与角色分离是必须理解的架构概念:
1 | 客户端 → 全局账号服务 → 认证 + 发 token → 选区 → 世界服 |
这样设计的好处:
- 角色状态可以在世界间迁移
- 封禁是全局的(一次封禁,所有世界都进不去)
- 支付是全局的
跨服(Cross-Server)
分服带来隔离,跨服玩法解决隔离带来的问题:
1 | World 1 ─┐ |
典型跨服玩法:跨服 PVP、跨服公会战、全服竞技场、全服排行、跨服聊天、赛季 tournaments、共享世界 Boss、跨服副本。
跨服的关键设计:
- 不需要访问完整玩家数据库。世界服发送玩家快照给跨服服务
- 跨服服务算完结果 → 各世界服各自应用奖励和排行变化
- 这样耦合度低,且世界服不需要开放数据库
玩家 ID 冲突
各服独立生成 ID 会冲突。两种解法:
- 组合标识符:
(worldId, playerId)全局唯一 - 全局唯一 ID:把
timestamp | worldId | sequence编码进一个数字
分布式 ID 策略上线后极难改,因为它会出现在:数据库记录、公会成员、消息体、交易流水、日志、跨服协议里。立项就要定好。
5.6 服务发现
| 方案 | 特点 | 适用 |
|---|---|---|
| 静态配置 | 写死在配置文件里 | 单机/少量节点,最简单 |
| etcd | Go 生态主流,K8s 生态标配 | Go 项目的推荐选择 |
| Consul | 服务发现 + KV + 健康检查一体 | 多语言团队 |
| Nacos | 阿里系,配置中心强 | Java 团队 / 国内环境 |
| K8s Service/DNS | 基础设施即发现 | 已上 K8s 时最省事 |
Pitaya 内建 etcd/Consul 服务发现,这是它相对 skynet 的现代之处。
5.7 部署形态
单机部署(起步阶段)
1 | ┌─────────────────────────────────────┐ |
单机能撑多少(Go + 优化良好):
- 状态同步游戏:3000~8000 长连接
- 帧同步游戏:10000+ 长连接(逻辑在客户端)
- 纯大厅/聊天:2 万+
容器化(中期)
1 | # 多阶段构建:编译 → 精简运行镜像 |
容器化的游戏服务端要点:
- 必须设
ulimit -n足够大(比如 100 万),容器默认 1024 远远不够 - 必须处理优雅退出(收到 SIGTERM 后停止接收新连接、落盘、退出)
- 健康检查端点:
/healthz返回当前连接数、tick 延迟 - 不要在容器里存数据(用卷或直接用外部 MySQL/Redis)
- 日志输出到 stdout,用 sidecar 收集
K8s + Agones(大规模专用服务器)
竞技游戏需要「每场比赛一个专用服务器进程」的模型(因为不同房间要互相隔离,避免一个房间崩了影响别人)。这是 Agones 的场景:
1 | Agones 提供: |
完整链路:
1 | 玩家排队 → Open Match 创建 Ticket |
Agones + Open Match 是 2026 年 Google 官方推荐的组合,两者是互补关系不是竞争关系:
- Agones 回答”比赛在哪里跑”
- Open Match 回答”谁和谁比赛”
- Nakama 回答”玩家在比赛之外做什么”(登录、好友、钱包、排行榜)
独立开发者的现实判断:Agones + Open Match 是一套重 Kubernetes 工程。如果你一个人做游戏,这套的运维负担可能超过收益。更务实的做法是:自己写一个轻量的”房间分配器”(用 Redis 存房间号 → 进程地址的映射,分配时从 Redis 池里取一个空闲进程),几十行代码就能覆盖 80% 的需求。
5.8 消息队列
游戏服务端里 MQ 的用途比 Web 少(游戏逻辑强调实时同步),但有几个场景离不开:
| 场景 | 用 MQ 吗 | 说明 |
|---|---|---|
| 玩家数据落库 | ✅ | 攒批写,削峰 |
| 跨服玩法 | ✅ | 世界服 → MQ → 跨服服务 |
| 支付回调 | ✅ | 异步处理 + 幂等 |
| 邮件/公告 | ✅ | 广播给在线玩家 |
| 战斗内实时逻辑 | ❌ | 必须同步调用,等结果 |
| 玩家操作响应 | ❌ | 必须同步返回 |
选型:
- NATS:轻量、Go 生态原生、性能好。游戏服务端首选
- Redis Streams:如果已经在用 Redis,不想多引入一个组件
- Kafka:需要高吞吐、持久化、消息回放时
- RabbitMQ:功能全但重,游戏场景用不上那么多特性
Pitaya 用 NATS(它本身基于 etcd + NATS),这是它的默认推荐。
第 6 章 核心系统拆解
这一章逐个拆游戏服务端的标准模块。每个模块给出设计要点 + 实现骨架 + 常见坑。
6.1 房间系统(Room)
房间是状态同步游戏的基本单元。一个房间 = 一个独立游戏实例 = 一个逻辑执行单元。
设计要点
| 决策 | 选项 | 建议 |
|---|---|---|
| 房间归属 | 独占进程 / 进程内多房间 | 进程内多房间(一个 Scene 进程管 N 个房间),房间数上限受内存限制 |
| 房间状态 | 全部内存 / 关键状态内存+DB | 战斗房间全内存,结束后结算 |
| 房间生命周期 | 固定时长 / 人数满足 / 空闲销毁 | 三者都要:到时结束、人满即开、空闲 5 分钟回收 |
| 空房间回收 | 立即 / 延迟 | 延迟回收(避免频繁创建销毁的抖动) |
1 | // 房间管理器:池化 + 延迟回收 |
房间的 tick 模型
一个房间一个 goroutine 是最简单可靠的方案(Go 的 goroutine 很轻,一个房间几千个 goroutine 没问题):
1 | func (r *Room) Run() { |
注意:所有房间共享一个物理进程时,不要让一个房间的 tick 阻塞其他房间。Go 里每个房间一个 goroutine 天然隔离,这是 Go 相比 Java 单线程事件循环的优势。
6.2 匹配系统(Matchmaking)
匹配算法的选择
| 算法 | 适用 | 复杂度 | 质量 |
|---|---|---|---|
| 随机 | 凑人数即可 | O(1) | 差(体验割裂) |
| 按段位分桶 | 有段位体系的游戏 | O(1) | 中(等待时间长) |
| Elo / Glicko 评分 | 竞技游戏 | O(n) | 好 |
| 基于等待时间放宽阈值 | 所有(必配) | — | — |
Elo 评分(最常用,可解释性好):
1 | 新分 = 旧分 + K × (实际结果 - 预期结果) |
Glicko-2 更准确(考虑了对手分差的不确定度),但实现复杂。对独立开发者,Elo 够用。
匹配池的设计(关键)
1 | 匹配池 = 一个「等待中的玩家队列」,按若干维度分桶 |
1 | // 匹配池:按 (模式, 区域, 段位段) 分桶 |
弹性匹配是竞技游戏匹配体验的关键。严格的 MMR 过滤器(±50 分)匹配质量高但离线时段排队时间爆炸。随等待时间逐步放宽是行业标准做法。
匹配池的分层顺序(性能优化)
匹配池的过滤条件应该按区分度从高到低排列,先缩小候选集:
1 | 1. 区域(region) ← 区分度最高,跨国玩家的延迟不可接受 |
常见错误:全局一个池,先按 mode 过滤 → 每次都要扫描全池。正确做法是池的分层就是嵌套的:
region/mode/段位段三级索引。
匹配流程(完整链路)
1 | 1. 玩家点开始 → 创建 Ticket(玩家ID、模式、评分、区域、进入时间) |
断线与补位
“玩家中途掉线”不是边缘情况,是常态(这是别人文档里的话,但确实如此)。
匹配流程必须支持:
- 服务器上报「我需要 1 个玩家」→ 生成新 Ticket 回到池里
- 匹配结果是可变的组,不是一次性的不可变集合
- 超时机制:分配后 N 秒内未到齐,释放位置或判定失败
6.3 AOI(Area of Interest,视野管理)
这是 MMO 服务端最核心的性能技术。没有 AOI,千人同屏的带宽需求是荒谬的。
问题量化
1 | 1000 人同图,每人每秒动 2 次,每次全图广播 1000 条: |
四级演进
| 方案 | 单次移动消息数 | 找”附近的人”成本 | 抗边界抖动 | 适用 |
|---|---|---|---|---|
| 全图广播 | O(n) | 0 | — | 原型、几十人小房间 |
| 视野半径裁剪 | O(k) | O(n) 全图扫描 | ❌ | 百人品级 |
| 九宫格(格子 AOI) | O(k) | O(格内人数) | 需配滞回 | 千人同图主流 |
| 十字链表 | O(k) | O(k),增量维护 | ✅ 天然平滑 | 超大规模 MMO |
| 十字链表 + 降频 + 位面 | O(k) 再打折 | O(k) | ✅ | 万人同图工业方案 |
九宫格实现
1 | // 2D 九宫格 AOI |
网格边长怎么定
经验值:网格边长 = 视野半径 × 1.5 ~ 2.0
- 太小(如 50m)→ 玩家频繁跨格,AOI 计算开销大
- 太大(如 1000m)→ 单格内人数过多,九宫格查找变慢
- 刚好 1.5~2 倍视野半径 → 九宫格能覆盖视野范围,但边缘不会有多余实体
三个必备的补丁
1. 滞回(Hysteresis)防抖
玩家在格子边界来回横跳,会产生 enter/leave 事件风暴。
1 | 进入视野半径 = 80m |
走进 80m 才算”出现”,走出 100m 才算”消失”。这是所有 AOI 实现的标配。
2. 十字链表(3D 场景)
云风(skynet 作者)提出的经典优化。不需要格子,每个实体在 X/Y/Z 三个轴上各维护一条有序链表,查询时沿链表向两边走,到半径外就停。
- 插入/删除:O(1) 均摊
- 查询:O(k),k 是视野内实体数
- 天然平滑,不需要滞回
1 | 轴标记设计: |
3. 远近分级降频
视野内再分三六九等:
| 距离 | 同步频率 | 内容 |
|---|---|---|
| 0~50m | 每帧(20Hz) | 完整状态 + 朝向 + 动画 |
| 50~200m | 5Hz | 位置 + 朝向 |
| 200~500m | 1Hz | 仅位置,不同步朝向 |
远处的敌人你根本看不出他是 20Hz 还是 5Hz 在动。
4. 位面分层(Layering)—— 最后的兜底
《魔兽世界》用的技术:同一个主城,玩家被悄悄分到多个平行位面,每个位面里同屏人数可控。你感觉”人山人海”,服务器眼里是”十几个小房间”。
国产 MMO 国战:同屏人数上限 + 超出的玩家降级为简化模型(”假人”,只同步位置不播动作);极端场景直接分线、分战场。
静态与动态分离
树木、石头、建筑等静态物体不需要 AOI 同步(客户端本地渲染或一次性加载)。AOI 只管理动态实体(玩家、怪物、NPC)。这一条能砍掉 50% 以上的实体管理开销。
6.4 战斗系统
职责划分
1 | 客户端 服务端 |
关键原则:服务端发的是「发生了什么」,不是「怎么播」。
1 | ❌ 服务端发:skill_fire_animation(id=123, pos=..., rot=...) |
服务端不需要知道客户端有没有这个特效资源。
伤害计算
1 | // 服务端权威的伤害计算 |
数值公式的通用原则(重要,你的策划会反复问):
| 原则 | 说明 |
|---|---|
| 减法有下限,乘法无上限 | base - def 会在 def > base 时变负;base * (1 - k) 天然在 [0, base] |
| 边际递减 | 防御/减伤用 k/(k+x) 形式,收益递减,不会出现”堆到一定量就无敌” |
| 整数结算 | 最终伤害取整,避免浮点误差累积 |
| 常量集中管理 | 所有魔法数字(Constant、系数)放配置表,不要硬编码 |
命中判定
1 | // 判定必须在服务端,且要考虑延迟补偿(第 3.5 节) |
6.5 背包与道具
这是你最熟悉的领域(数据库 CRUD),但有几个游戏特有的规则:
关键设计
| 规则 | 说明 |
|---|---|
| 唯一物品 ID vs 模板 ID | itemId=10001(模板,”红药水”)vs uid=880123456(实例,”玩家 42 的第 3 瓶红药水”) |
| 堆叠 | 同模板可堆叠(count=99),不同实例独立 |
| 属性随机词条 | 装备的词条是实例级属性,同模板不同实例属性不同 |
| 背包容量 | 格数限制(slots),也可能有限重 |
| 交易/拾取 | 需要事务或状态机 |
| ID 唯一性 | uid 必须全局唯一 → 分布式 ID 方案在这里发挥作用 |
1 | // 背包模型:模板 + 实例分离 |
背包操作必须在服务端原子完成。客户端显示的背包是服务端的镜像——永远不要相信客户端发来的”我有多少个物品”。
6.6 排行榜
Redis ZSET 是标准答案。这个用法你在 Web 开发里应该已经很熟了:
1 | ZADD rank:leaderboard:arena:1 <score> <playerID> |
1 | // 全局排行榜:各世界服上报,全服服务合并 |
注意事项:
- 只保留前 N 名(
ZREMRANGEBYRANK),否则 ZSET 会无限膨胀 - 分数相同时用
playerID做二级排序,保证顺序稳定 - 排行榜更新频率可以低(10 秒一次足够),不需要实时
- 分数变化时异步推送给玩家(”你上榜了!”)
6.7 聊天与社交
聊天系统
| 类型 | 实现 |
|---|---|
| 世界频道 | 广播到当前场景/地图的所有人(受 AOI 约束,因为你只能看到视野内的人) |
| 公会频道 | 广播到公会成员(公会成员列表缓存) |
| 私聊 | 点对点,直接投递到目标连接的发送队列 |
| 系统消息 | 定向推送(playerID → conn 映射) |
1 | // 定向推送:按 playerID 找连接(Gateway 层) |
社交系统(好友、公会、师徒)本质是关系图 + 状态数据:
1 | 好友关系:存 MySQL(关系型、需要查询"我的好友列表")+ Redis 缓存(热点) |
6.8 任务与活动
这是策划的主战场,也是最容易设计失误的地方。
任务系统的经典设计:状态机
1 | 任务状态:未接取 → 进行中 → 已完成(可提交)→ 已提交(已领奖) |
1 | type Quest struct { |
关键设计原则:
- 任务进度用事件驱动更新,不是轮询扫描
1
玩家击杀怪 → 触发事件 OnMonsterKilled(monsterID) → 广播给相关任务
- 主线任务串行(做不了 B 才解锁 A),日常任务并行
- 限时任务用时间戳判定,不用定时器(定时器在重启后会丢)
活动系统
活动是配置驱动的运营内容。正确的做法:
1 | 活动配置表(Excel)→ 导表工具 → 活动配置数据 |
不要为每个活动写代码。 这是独立开发者的生死线——活动会有几十个,每个写代码就是几十个维护负担。配置化是唯一出路(第 9 章)。
6.9 支付与充值
这是最容易出资损事故的地方,必须最谨慎。
1 | // 支付回调处理:幂等 + 严格校验 |
铁律:
- 永远以自己系统的订单状态为准,不信任渠道传来的商品 ID / 金额
- 幂等键用渠道订单号(全局唯一且稳定)
- 发货要有唯一约束(数据库层面),不能只靠 Redis 锁
- 先发货后标记状态(反过来会漏发);发货失败要能重试
- 金额用整数(分),绝不用浮点
- 所有支付相关表要有对账任务(每日跟渠道核对)
在 Laravel 里做支付回调,经验是完全可迁移的——幂等、签名验证、订单状态机、对账,这些你在快手开放平台对接时应该都做过。这是你转游戏服务端最直接可用的经验。
第 7 章 数据存储:热温冷分离
7.1 三级存储模型
1 | ┌───────────────────────────────────────────────────────────────┐ |
7.2 Redis 在游戏服务端的用法
你在 Web 开发里用 Redis 的经验,几乎 100% 可以直接迁移。这是你最大的存量资产之一。
| 用途 | 数据结构 | 关键点 |
|---|---|---|
| 玩家快照 | String (JSON) | TTL 1 小时,配合 30 秒定时刷新 |
| 会话 token | String | 登录时生成,TTL = 会话有效期 |
| 在线状态 | String / Set | online:12345 TTL 60s,心跳续期 |
| 排行榜 | ZSET | ZADD 更新分数,ZREVRANK 查排名 |
| 分布式锁 | String + SET NX EX |
单个 Redis 实例是 SPOF,慎用 |
| 匹配池 | List / ZSET | 按等待时间排序 → 用 ZSET(score = 入池时间戳) |
| 房间映射 | Hash / String | room:10086 → ip:port |
| 道具/配置缓存 | String / Hash | 配表数据常驻 |
| 限流计数 | String (INCR) | 滑动窗口用 ZSET 更准 |
| 幂等标记 | String (SETNX) | 支付回调、领奖去重 |
1 | // 玩家快照的读写 |
Redis 的两个生产级坑
1. 分布式锁不是银弹
SET key val NX EX 30 不是原子的 fail-safe 逻辑(业务执行超过 30s 时锁已过期)。需要看门狗或用 Redlock。但更重要的是:不要用 Redis 锁做”必须成功”的事(比如发货),应该用数据库唯一约束兜底。
2. 单点故障
单 Redis 实例重启 = 所有在线玩家状态丢失。生产环境至少:
- 主从 + Sentinel,或
- Redis Cluster,或
- 云厂商托管 Redis(阿里云/腾讯云 Redis 有高可用版)
7.3 MySQL 表设计
核心表清单
1 | -- 账号表(全局,不分世界) |
uk_channel_order这个唯一索引就是你的资损防线。任何重复发货都会在这里被数据库挡住。
分库分表
分片键选择:
| 数据 | 分片键 | 理由 |
|---|---|---|
| 角色表 | player_id % N 或 world_id |
查角色基本按 ID 或世界 |
| 物品表 | player_id % N |
查询总是”这个玩家有什么” |
| 订单表 | player_id % N |
同上 |
| 账号表 | account_id % N |
登录按 ID |
| 日志表 | 按时间分(月/周) | 归档场景 |
1 | // 分片路由 |
什么时候才需要分库分表:
- 单表超过 1000 万行 或 10GB
- 单库连接数打满(MySQL 默认最大连接数 151,线上建议 500)
- 写入 TPS 超过 5000
独立开发者的建议:先不分。你的游戏在达到 1000 万玩家之前,单库 + 索引优化 + Redis 缓存完全够用。等真的慢了再上分库分表,中间加一层 Router 抽象,届时迁移成本可控。过早的分库分表是纯浪费。
写入策略
这是游戏服务端数据层最关键的设计。
1 | ❌ 直写:每收到一个操作就 UPDATE players SET hp=... WHERE id=... |
1 | // 攒批写入 |
分层写盘的延迟设计:
| 数据类型 | 落 Redis 延迟 | 落 MySQL 延迟 | 丢失可接受度 |
|---|---|---|---|
| 位置坐标 | 不落 | 不落 | 完全可接受(重连后校正) |
| 血量/战斗状态 | 不落 | 战斗结束才落 | 可接受 |
| 经验/金币 | 30 秒 | 5 分钟 | 轻微(可补偿) |
| 背包物品 | 30 秒 | 5 分钟 | 轻微(可补偿) |
| 充值/付费道具 | 立即 | 立即 + 事务 | 不可接受 |
| 订单流水 | 立即 | 立即 | 不可接受 |
7.4 分布式 ID
游戏内 ID 的特殊性:会出现在数据库、消息体、公会成员、交易记录、日志、跨服协议里。改 ID 策略 = 改整个系统。
方案对比
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 数据库自增 | 简单 | 分库冲突、暴露业务量 | 单库场景 |
| 雪花算法 (Snowflake) | 趋势递增、无中心 | 依赖时钟、暴露时间 | 通用首选 |
| 号段模式(数据库分配段) | 无时钟依赖 | 需要 DB | 极端严苛场景 |
| 业务维度编码 | 自带语义 | 长度受限 | 玩家 ID、订单 ID |
雪花 ID 的结构:
1 | 0 | 41位时间戳(ms) | 10位工作节点 | 12位序列号 |
1 | // Go: 雪花ID生成器 |
时钟回拨是雪花 ID 的经典事故:NTP 校时把时间往回调,可能导致 ID 重复。生产环境要:① 用单调递增的时钟源 ② 检测到回拨时等待或报警 ③ nodeID 用机器标识自动分配(Redis 分配)而不是手写。
游戏里的 ID 建议分层:
1 | 玩家 ID:雪花 ID(或 timestamp|worldId|sequence 编码) |
注意区分:配置表 ID 是策划维护的短数字(人类可读),运行时 UID 是系统生成的。这两个不能混。
7.5 缓存策略
Cache-Aside(最常用)
1 | 读:查缓存 → miss 查 DB → 回填缓存 → 返回 |
为什么写是”删缓存”而不是”更新缓存”:
- 更新缓存可能和并发写产生顺序问题(后写的先更新缓存)
- 删除缓存是”下次读时重建”,最终一致
缓存穿透 / 击穿 / 雪崩(你在 Web 里应该都处理过):
| 问题 | 现象 | 解法 |
|---|---|---|
| 穿透 | 查不存在的 key,每次都打到 DB | 缓存空值 + Bloom Filter |
| 击穿 | 热点 key 过期瞬间,大量请求打到 DB | 互斥锁重建 / 逻辑过期 |
| 雪崩 | 大量 key 同时过期 | 过期时间加随机抖动 |
游戏特有的缓存:配置表
配置表是游戏服务端最热的数据(技能属性、怪物属性、道具定义、关卡配置)。它应该是:
1 | ① 进程启动时全量加载进内存(只读) |
1 | // 配置管理器:热重载 + 原子替换 |
原子指针替换是 Go 1.19+ 提供的标准做法,读侧完全无锁。这比”加锁读 map”快一个数量级,在每秒几万次读的场景下差异显著。
7.6 数据库选型补充
| 存储 | 用途 | 说明 |
|---|---|---|
| MySQL | 核心业务数据 | 关系完整、事务、你的主场 |
| Redis | 热数据、排行榜、池子 | 一切需要”快”的地方 |
| MongoDB | 日志、动态结构数据 | ET 框架用 BSON;配置复杂的文档型数据 |
| ClickHouse / Doris | 日志分析、BI | 玩家行为分析、运营报表 |
| 对象存储 (OSS/S3) | 存档、回放、日志归档 | 大文件 |
| Kafka | 埋点上报、离线计算 | 数据分析链路 |
对你的迁移价值:你在 FastAdmin/Laravel 里的 MySQL + Redis 经验直接复用,这是最不需要重新学的一块。
第 8 章 客户端协同:引擎与 Netcode 选型
8.1 引擎选择
| 引擎 | 语言 | 优势 | 劣势 | 适合 |
|---|---|---|---|---|
| Unity | C# | 生态最大、asset store 最全、招聘容易、ET 双端共享 | 包体大、移动端优化累、许可证政策变动 | 中重度、跨平台 |
| Unreal | C++/蓝图 | 画面顶级、内置网络同步、编辑器强 | 学习成本极高、编译慢、包体巨大 | 3A、主机 |
| Cocos Creator | TypeScript | 2D/轻3D优秀、国内生态好、TS 便于与 Node 服务端共享类型 | 3D 能力弱于 Unity | 休闲、卡牌、2D、微信小游戏 |
| Godot | GDScript/C# | 开源免费、轻量、编辑器友好 | 商业化案例少、招聘难 | 独立游戏、2D |
| 自研引擎 | 任意 | 完全可控 | 个人项目的最大陷阱 | 除非你已经是引擎工程师 |
独立开发者的绝对红线:不要自研引擎。 你要学的东西已经够多了。引擎的自研工作量通常是游戏本身的 3-5 倍。
我的建议:
- 2D / 休闲 / 卡牌 / 微信小游戏 → Cocos Creator(TypeScript,且 Node 服务端可共享类型定义)
- 3D 中重度 / 需要双端共享逻辑 → Unity(配合 ET)
- 极致 2D 独立作 → Godot
8.2 Netcode 选型(Unity)
这是 Unity 多人游戏最关键的决策。先定同步模型,再选 SDK。
| 方案 | 同步模型 | 授权 | 自托管 | 适用 |
|---|---|---|---|---|
| Netcode for GameObjects (NGO) | 状态同步 + 预测 | 官方免费 | ✅ | 官方首选,Unity 6 生态 |
| Netcode for Entities (NCE) | DOTS/ECS 状态同步 + Ghosts | 官方免费 | ✅ | 海量实体(MMO)、有 DOTS 基础 |
| Mirror | 状态同步 | MIT | ✅ | 存量项目、成熟生态 |
| FishNet | 状态同步 + 内置预测 | 免费 + Pro | ✅ | 要预测但不想租云 |
| Photon Fusion 2 | 状态同步 + 预测 + 回滚 | 商业(按 CCU) | ❌(托管) | 不想运维基础设施 |
| Photon Quantum | 确定性回滚 | 商业 | ❌ | 确定性竞技游戏 |
选型决策
1 | 游戏类型 + 你的约束 |
三个必须知道的坑
1. 所有方案都自带网络模拟器,从第一天就用
一个只在 localhost 0ms 跑过的游戏,在第一个跨时区的真实玩家接入时就会崩掉——而那时那些错误的假设已经成了承重结构。
四种方案都提供模拟器。测试矩阵至少包含:50ms / 100ms / 200ms 延迟,1% / 5% 丢包,抖动。
2. 不要同步能推导出来的东西
发送输入 + 权威状态,让客户端自己算表现。每帧给每个道具同步一个 transform,会在你察觉之前烧光带宽预算。
3. 网络化的 spawn 要池化
Spawn 在网络上的成本比本地高,而那些拖垮单机帧率的分配问题,在加上 spawn 消息广播后会更糟。
所有方案都有对象池。频繁创建的网络对象必须走池。
决策表(2026 版)
| 方案 | 最适合 | 主要优势 | 主要顾虑 |
|---|---|---|---|
| Photon Fusion | GameObject 动作、FPS/TPS、合作 | 预测/重演、延迟补偿、多拓扑 | Photon 依赖、CCU 成本、专用服务器是另一套 |
| NGO + MPS | Unity 6、UGS 生态的合作游戏 | Unity 原生集成、Sessions/Relay/Lobby | MPS Sessions 要 Unity 6;高级动作预测非开箱即用 |
| Netcode for Entities | DOTS/ECS、大量实体、专用服务器 | Ghosts、预测、延迟补偿 | ECS 承诺、学习曲线、重演成本 |
| FishNet | GameObject、要看源码、自托管 | 预测、服务器权威、传输层灵活 | 自定义许可;周边基础设施和 QA 仍归你 |
| Mirror | 存量代码、要求 MIT、自托管 | 快照插值、延迟补偿 | 通用预测仍是 Researching |
| Photon Quantum | 确定性竞技 | 确定性 ECS + 回滚 | 模拟逻辑要与普通 Unity 物理分离 |
| Nakama | 后端驱动多人 | 认证、存储、匹配、聊天、权威对局循环 | 不是 3D 动作状态同步的即插即用方案 |
给独立开发者的推荐:
- 要预测 + 自托管 → FishNet
- 要官方支持 + 不做重度动作 → NGO
- 不想运维 → Photon Fusion 2(但算清成本)
- 和 ET 一起用 → ET 自带网络层(支持可靠 UDP/TCP/WebSocket),不用选第三方 netcode
8.3 通信桥:Unity ↔ 服务端
如果服务端不是 C#,需要定义协议。三种方式:
方式 A:Protobuf(推荐)
1 | proto/game.proto |
优点:跨语言、schema 单一真相源、向后兼容、生成的代码类型安全。
Unity 注意事项:
- Unity 支持 .NET Standard 2.1,用
protobuf-net或官方Google.Protobuf都行 - 官方
Google.Protobuf在 IL2CPP 下需要protoc生成 C# 代码(不要用反射) - 生成的代码要放进
Assets/Plugins/,注意 asmdef 定义
1 | syntax = "proto3"; |
注意 state_mask 这个设计——状态同步的增量更新靠它。服务端只设置发生变化的字段对应的位,客户端按位解析。
方式 B:FlatBuffers(帧同步 / 高频)
零拷贝读取,帧同步场景收益明显。代价是 schema 演进更麻烦,Unity 侧要引入 com.google.flatbuffers 库。
方式 C:手写二进制(极致性能场景)
只保留消息头 + 字段直写。不推荐起步用——维护成本高,且 Protobuf 已经足够。
8.4 客户端与服务端的职责边界
这张表是防止客户端被写崩的关键:
| 职责 | 客户端 | 服务端 | 说明 |
|---|---|---|---|
| 界面渲染 | ✅ 唯一职责 | ✗ | |
| 动画/特效播放 | ✅ | ✗ | 服务端只发”触发了 skill_x” |
| 玩家位置(本地) | ✅ 预测 | ✅ 权威 | 客户端预测,服务端校正 |
| 玩家位置(他人) | ✅ 插值 | ✅ 广播 | |
| 血量/蓝量显示 | ✅ | ✅ 权威值 | 客户端可以插值显示,但服务端是唯一真相 |
| 伤害计算 | ❌ 只显示 | ✅ 唯一计算方 | |
| 技能 CD 显示 | ✅ 本地预测 | ✅ 权威 | |
| 背包数据 | ✅ 缓存显示 | ✅ 权威 | |
| 掉落/奖励 | ❌ | ✅ 唯一决定方 | 客户端请求”我要开箱”,服务端决定开出了什么 |
| 匹配/组队 | ✅ 发起 | ✅ 分配 | |
| 充值 | ✅ 发起支付 | ✅ 发货 | 支付结果以服务端为准 |
铁律:客户端只负责”看”和”表达意图”,服务端负责”是”和”给”。 一旦你在客户端计算了任何影响游戏结果的东西,游戏就出现了可被利用的漏洞。
8.5 热更新
这是游戏项目运营的核心能力——出问题时不能等应用商店审核(iOS 审核可能一周)。
三大热更对象
| 对象 | 方案 | 说明 |
|---|---|---|
| 配表 | 走 CDN 的 JSON/BIN | 最简单,必须有 |
| 资源 | Unity Addressable + 分包 + CDN | 资源依赖闭包自动处理 |
| 代码 | HybridCLR(C# 热更) | 纯 C# 代码热更,无需重新打包 |
| ILRuntime | 早期方案,有性能损耗 | |
| Lua (xLua/Puerts) | 逻辑用 Lua 重写,热更最灵活 |
配表热更的完整链路
1 | 策划改 Excel → 提交 Git → CI 触发导表 → 生成 JSON/BIN → 上传 CDN |
关键:版本号 + 灰度
1 | config_version: 1001 |
服务端启动时拉一次,之后按版本号变更推送。客户端启动时带本地版本号请求,差量更新。
代码热更(HybridCLR)的现实
- 热更的代码不能有 AOT 泛型限制(HybridCLR 会补齐,但要注意)
- 热更会增加包体和复杂度,不是所有项目都需要
- 如果你的游戏只有运营活动更新 → 配表热更就够了
- 如果你的游戏要频繁改战斗逻辑 → 代码热更价值很大
独立开发者建议:先只做配表热更。这是 90% 的运营需求(改数值、加活动、修数值 bug),成本最低。代码热更等你的项目真的需要时再加。
8.6 平台 SDK 接入
如果你要做商业发行,需要接各平台的 SDK:
| 平台 | 需要的能力 |
|---|---|
| 微信小游戏 | 登录(wx.login)、支付、云开发、广告、分享 |
| 抖音小游戏 | 同上 + 短视频能力 |
| 抖音/快手小游戏 | 登录、支付、广告(你有快手平台经验,这里是优势) |
| iOS | 苹果内购(IAP)严格合规、Sign in with Apple |
| Android | 支付渠道多、Google Play 结算合规 |
你已有的快手开放平台对接经验在这里是稀缺资产——渠道 SDK 接入、支付回调、幂等处理、订单对账,这些坑你都已经踩过了。
技术建议:
- SDK 接入放在独立的”平台适配层”,游戏核心逻辑不直接依赖任何渠道 SDK
- 用一个
IPlatformAdapter接口,各渠道实现一份 - 这样换渠道、加渠道不用动核心代码
1 | // 平台适配层 |
这个抽象层的设计,你在 Laravel 里做多渠道支付时应该写过同样的东西。直接复用你的经验。
第 9 章 配表与数值流水线
9.1 为什么配表是独立开发者的生死线
想象一下没有配表的世界:
1 | // ❌ 硬编码 |
策划想调一下”烈焰箭伤害从 150 改成 160”,你得:改代码 → 重新编译 → 重新发布 → 等 iOS 审核 → 上线。两周。
有配表的世界:
1 | 策划改 Excel → 提 Git → CI 导表 → 上传 CDN → 服务端拉取生效 |
5 分钟。 一个月改 20 次数值,就是 40 小时 vs 2 小时。
独立开发者的游戏,运营期的数值调整频率会远超你的预期。 配表不是锦上添花,是让你能持续运营的前提。
9.2 Luban:推荐的配表工具
Luban 是目前国内游戏行业最主流的开源配表工具(Unity 生态,也支持其他引擎)。
| 特性 | 说明 |
|---|---|
| 类型系统 | 不只是格式转换,是给配表加了真正的类型系统 |
| OOP 继承 | 支持表行继承变体 → 行为树、技能、任务、副本定义可以表达变体,而不是把变体拍平成可空列 |
| 校验规则 | ref 引用检查、path 路径检查、range 范围检查 |
| 多格式输入 | Excel (csv/tsv/xls/xlsx/xlsm)、json、xml、yaml、lua |
| 多格式输出 | 二进制、json、bson、xml、lua、yaml |
| 代码生成 | c#、java、go、cpp、lua、python、javascript、typescript、rust、php、erlang、godot |
| 无反射 | 生成的代码不调反射接口 → 兼容混淆/加固工具(HybridCLR、ILRuntime、xlua、puerts 的硬约束) |
| MIT 许可 | 可商用 |
为什么”校验”比”导出”重要:
转换器会愉快地导出一张技能表引用了不存在的怪物 ID 的表,你到运行时才发现。带 ref 检查的流水线会在构建时就失败。
这个差异在项目规模变大后是决定性的。
Luban 的成本:schema 定义是新增的维护物(README 明确说不会自己写出来)。工具链的复杂度换来的是「转换器 → 编译器」的质变。
9.3 Luban 风格的表结构
1 | 第一行:字段名(英文 key) ← 程序用 |
这就是手游行业惯例的「四行表头规范」。Luban 在此基础上扩展了类型系统和校验。
9.4 配置的加载与热更
1 | // 服务端启动时加载配置 |
关键设计点:
- 原子替换(
atomic.Pointer)→ 读侧零锁,热路径无开销 - 加载失败保留旧配置 → 配置服务挂了游戏还能玩
- 版本号 → 客户端和服务端能判断是否一致
- 配置在内存里是只读的 → 不需要加锁(除了热更时刻的原子替换)
9.5 数值设计的基本原则
这部分不是纯技术,但决定游戏好不好玩,而且独立开发者最容易犯错。
1. 数值要有”锚点”
| 层级 | 作用 | 示例 |
|---|---|---|
| 基准值 | 一切以此为参照 | 1 级小怪 HP = 100 |
| 成长值 | 随等级/装备的增量 | 每级 +15% |
| 阈值 | 明确的关卡 | 10 级是一个明显的”墙” |
最常见的错误是”每个数值都是拍脑袋定的”。定一个基准值,然后用比例关系推出其他所有值。
2. 用公式,不用硬编码
1 | 攻击力 = 基础攻击 × (1 + 装备加成) + 武器基础值 |
所有公式的输入(基础值、系数、减伤上限)都应该在配表里。策划能改公式里的数字,但不能改公式本身。
3. 减法有下限,乘法无上限
1 | ❌ 伤害 = 攻击力 - 防御 // 防御 > 攻击力 时出现负伤害 |
4. 边际递减
1 | ❌ 防御 = 基础防御 × 等级 // 线性成长,堆到一定量就无敌 |
5. 敏感度分析
改一个数值前,先问:改 10% 会发生什么?
| 数值 | 改 10% 的影响 | 风险 |
|---|---|---|
| 怪物 HP | 战斗时长 ±10% | 低 |
| 玩家攻击力 | 战斗时长 -10%(伤害占比高时更明显) | 中 |
| 技能冷却 | DPS 变化可达 ±30%(有攻速buff时) | 高 |
| 暴击率 | 期望伤害变化非线性 | 高 |
| 资源产出 | 经济系统可能失衡 | 高 |
乘区(乘法区)比加区敏感度高。伤害 = 攻击力 × 技能系数 × 暴击系数 三个乘区各改 10%,总伤害变 33%。
设计原则:一个数值调整能造成的总影响应该被控制在 ±20% 以内。想做大幅调整,一次只动一个乘区。
6. 经济系统的闭环
1 | 产出(副本/任务)→ 消耗(装备强化/商店)→ 回收(分解/交易税) |
产出 > 消耗 → 通胀 → 后期数值全废
产出 < 消耗 → 紧缩 → 玩家没资源 → 卡在某个进度
必须做”经济模拟”:跑一个模拟器,用不同活跃度的玩家模型跑 30 天,看资源曲线。这是游戏数值设计的标准工具,很多团队会专门写。
9.6 版本管理与灰度
配表的版本管理:
1 | config/ |
建议:
- Excel 源表入 Git(可以看 diff,能回滚)
- 导出产物走 CI 产物存储(不要入 Git,避免冲突)
- 每次发布生成一个版本号,记录”这个版本改了什么表”
灰度发布:
1 | v1001 全量 |
游戏行业的灰度靠配置中心下发版本号实现,服务端根据玩家 ID hash 决定用哪个版本。这个机制从第一天就要建,后加会很难。
9.7 客户端与服务端的配置一致性
这是配表最常见的线上事故来源:客户端显示的技能描述是 150 伤害,服务端算出来是 180。
防御措施:
- 配置表用同一个 Luban schema 生成两端代码 → 结构一致
- 配置版本号在登录握手时交换 → 不一致时告警
- 关键数值在服务端再校验一次 → 即使不一致也不至于出错
1 | // 登录握手时校验配置版本 |
第 10 章 运维与可观测
10.1 部署
进程管理:Supervisor vs systemd vs pm2
| 方案 | 适用 | 备注 |
|---|---|---|
| systemd | Linux 生产首选 | 免费、自带日志、崩溃重启、Restart=always |
| Supervisor | 多进程管理 | Web 场景常见,游戏服也可以 |
| pm2 | Node.js | 你已经在用(狂野战车项目) |
| Docker + K8s | 容器化 | 弹性伸缩、编排 |
systemd 单元示例:
1 | # /etc/systemd/system/gameserver@.service |
1 | # 启用多个实例 |
注意
LimitNOFILE:默认 1024,一个连接占一个 fd,1000 连接就满了。游戏服务端必须调到 10 万以上。这是最常见的”服务莫名其妙连接不上”的根因。
Nginx 反向代理(WebSocket)
1 | upstream game_ws { |
踩坑提醒:
Connection "upgrade"少了 → WebSocket 握手失败,返回 400proxy_read_timeout默认 60s → 长连接被 nginx 掐断。心跳 30s 的话刚好边缘,建议调到 300s- 必须透传真实 IP(
X-Real-IP),否则你的 IP 限流/封禁全部失效 - WebSocket 有连接数限制,要调
worker_connections和worker_processes
优雅退出
1 | // 收到 SIGTERM 后:停止收新连接 → 等现有连接结束 → 落盘 → 退出 |
容器环境的必答题:K8s 发送 SIGTERM 后默认只等 30s,超时就 SIGKILL(数据直接丢)。所以:
- 监听 SIGTERM
- 在 30s 内完成落盘
- 设置
terminationGracePeriodSeconds: 60留足时间
10.2 日志
日志分级
| 级别 | 用途 | 游戏服务端特别说明 |
|---|---|---|
| FATAL | 服务无法继续 | 立即告警 |
| ERROR | 单个操作失败 | 告警 + 堆栈 |
| WARN | 降级、异常但不影响 | 关注 |
| INFO | 关键生命周期事件 | 游戏服务端应该很克制 |
| DEBUG | 开发细节 | 生产关闭 |
| TRACE | 逐包日志 | 生产绝对关闭 |
游戏服务端日志的特殊要求:
- 不要每包都打日志(每秒几万包会把磁盘打满)→ 用采样或计数
- 战斗日志要能复盘 → 记录关键事件(技能释放、死亡、掉线),而不是每个位移
- 玩家操作日志要有玩家 ID → 排查客诉时唯一有用的信息
- 日志要能关联 traceID → 一次请求跨多层的完整链路
1 | // 带玩家上下文的日志 |
日志落盘
| 方案 | 说明 |
|---|---|
| 文件 + logrotate | 简单,但要处理磁盘满 |
| stdout → 采集器 | Docker/K8s 标准做法(推荐) |
| Loki + Promtail | 轻量,适合中小规模 |
| ELK | 功能全,重 |
| 云厂商日志服务 | 阿里云 SLS / 腾讯云 CLS,省事 |
磁盘满是最常见的”半夜服务挂了”的原因。必须有:
- 日志轮转 + 保留策略
- 磁盘使用率告警(>80% 告警,>90% 紧急)
- 定期清理历史日志
10.3 监控指标
游戏服务端要监控的指标(按重要性):
第一梯队(挂了必须知道)
| 指标 | 为什么 | 告警阈值建议 |
|---|---|---|
| 在线玩家数 | 掉了一半就是掉线了 | 环比跌 30% |
| tick 延迟 | 游戏逻辑卡顿的直接指标 | P99 > 50ms |
| 连接成功率 | 玩家进不来 | < 95% |
| 进程存活 | 基础 | 立即 |
| CPU / 内存 | 资源饱和 | CPU > 85% 持续 5min |
第二梯队(影响体验)
| 指标 | 说明 |
|---|---|
| 消息延迟分布 | 客户端到服务端再返回的耗时(P50/P99) |
| 广播消息量/秒 | 带宽趋势 |
| Redis 命中率 | 缓存失效的早期信号 |
| MySQL 慢查询数 | 数据层压力 |
| 匹配等待时长 | 玩家体验的核心指标 |
| 消息队列堆积 | 异步链路堵了 |
| 登录成功率 / 耗时 | 开服/更新的关键路径 |
第三梯队(业务)
| 指标 | 说明 |
|---|---|
| 付费转化率 | 商业健康度 |
| 留存率 | 商业健康度 |
| 道具产出/消耗比 | 经济系统健康度 |
tick 延迟是游戏服务端独有的、最重要的技术指标:
1 | // 每个 tick 记录耗时 |
游戏客户端卡顿 90% 的原因是服务端 tick 抖动。这个指标必须从第一天就有。
10.4 压测
工具选型
| 工具 | 适用 | 特点 |
|---|---|---|
| wrk | HTTP/TCP 基准 | 多线程 + 事件驱动,输出分位数 |
| vegeta | 脚本化多阶段压测 | 原生 Go,输出 JSON/CSV,支持自定义负载模型和熔断判断 |
| ghz | gRPC 专项 | 内置 proto 解析、实时指标 |
| k6 | 复杂链路(登录→下单→支付) | JavaScript 脚本、指标丰富、可集成 CI |
| hey | 快速验证单接口 | 轻量、支持 HTTP/2 |
| 自研机器人 | 真实游戏行为 | 最真实,但成本最高 |
游戏服务端压测的关键:不能只压 HTTP 接口,要压”游戏行为”。
1 | // 自研压测机器人:模拟真实玩家行为 |
压测数据必须包含三类黄金指标:
- 吞吐量(RPS / 每秒消息数)
- 延迟分位数(P50 / P90 / P99 / P999)
- 错误率
只看平均值是最大的陷阱。平均延迟 100μs 听起来不错,但如果 P99 是 10ms,1% 的用户体验就是灾难。更隐蔽的是平均值会掩盖双峰分布——90% 走缓存只要 10μs,10% 走磁盘要 1ms,平均 109μs 看着还行,但那 10% 的用户已经在骂娘。
压测的标准化流程
1 | # 1. 开启 profiling |
火焰图的价值在于打破猜测:
你以为瓶颈在数据库查询,火焰图一看 80% 的时间花在 JSON 序列化上。你以为锁竞争很严重,火焰图一看大部分时间在内存拷贝。
没有火焰图的性能分析都是盲人摸象。 每次遇到性能问题第一件事就是抓火焰图。
Linux 侧的工具:
1 | # 系统级延迟分布(一眼看出是单峰、双峰还是长尾) |
10.5 分布式追踪
当一个请求跨了 Gateway → 逻辑服 → DBProxy → Redis,出了问题你需要知道卡在哪一层。
方案:
- OpenTelemetry(现在的事实标准,游戏服务端用得还不多但值得)
- SkyWalking(Java 生态强,Go 支持一般)
- Jaeger(CNCF 项目)
- 简单方案:自己传 traceID(成本低,够用)
1 | // 简单方案:在消息头里带 traceID |
10.6 故障排查手册
这套流程应该在项目开始时就写下来,而不是出事才现查。
1 | 1. 玩家投诉"卡" / "掉线" |
几个救命工具:
1 | # Go: 抓 goroutine 栈(查死锁/阻塞) |
10.7 灰度与回滚
游戏服务端的回滚比 Web 危险,因为有状态:
| 回滚对象 | 难度 | 方法 |
|---|---|---|
| 代码 | 中 | 灰度发布:新版本只给 10% 玩家,观察后放量 |
| 配表 | 低 | 版本号回退,配置中心推旧版本 |
| 数据结构 | 高 | 必须向前兼容(新代码能读旧数据),用扩展式迁移 |
数据库变更的铁律:
- 只加不改不删(加字段可以,改字段类型不行,删字段不行)
- 新旧代码要能共存一段时间(灰度期间 10% 玩家跑新代码,90% 跑旧代码)
- 删字段要等所有实例都升级后,隔一个版本再删
- 大表变更用在线 DDL(gh-ost / pt-online-schema-change),不要
ALTER TABLE锁表
在 Laravel 里做迁移的经验直接可用,加上”游戏服务不能停机”这个约束就完整了。
第 11 章 反作弊与安全
11.1 威胁模型
先明确你要防谁:
| 攻击者 | 能力 | 目标 | 防御 |
|---|---|---|---|
| 普通外挂(内存修改器) | 改客户端内存/速度 | 秒杀、无敌、加速 | 服务端权威(最重要) |
| 协议外挂(抓包改包) | 自定义客户端 | 改伤害、刷道具 | 签名校验 + 服务端校验 |
| 脚本工作室 | 自动化操作 | 挂机刷资源 | 行为检测 + 风控 |
| DDoS | 网络层 | 打瘫服务 | 限流 + 高防 + 架构隔离 |
| 内部人员 | 访问数据库/GM | 刷资源、导数据 | 权限分级 + 操作审计 |
核心原则:客户端的一切都不可信,只有服务端的计算结果是真的。
11.2 逐层防御
1 | ┌─────────────────────────────────────────────────────────┐ |
11.3 服务端权威的落地
这个表是开发时反复要查的:
| 客户端上报 | 服务端必须校验 |
|---|---|
| 移动方向 | 是否在移动中、是否被控速、是否穿墙(碰撞检测) |
| 攻击目标 | 距离、视线、是否同场景、是否存活、是否可攻击(阵营) |
| 技能释放 | CD、消耗(蓝/怒)、是否被沉默、是否前置状态满足 |
| 拾取物品 | 是否在拾取范围内、是否已存在、归属权 |
| 使用道具 | 是否拥有、数量是否足够、是否在限制状态 |
| 购买商品 | 价格、数量上限、货币是否足够、限购是否超 |
| 结算结果 | 一律不接受客户端上报的结果 |
1 | // 攻击请求的服务端校验(完整版) |
注意代码里完全没有用到
req.Damage之类的字段——如果客户端协议里有这个字段,删掉它。留着就是给外挂的邀请函。
11.4 速度外挂的检测
客户端加速(快跑)是最常见的外挂,且不涉及改包。检测方法:
1 | 服务器记录玩家的位置变化序列 |
关键难点:网络延迟和丢包会让”客户端位置跳变”,不能简单判定。正确做法:
- 服务端自己跑移动逻辑(状态同步的标准做法)→ 客户端位置只用于显示,服务端位置才是权威
- 平滑检测:连续 N 次超速才判定,避免误判网络抖动
- 阈值放宽:理论最大速度 × 1.2(留出网络误差)
11.5 DDoS 防护
游戏服务端是 DDoS 的重灾区,因为长连接本身就吃资源(一个连接占内存 + 心跳开销)。
| 层次 | 措施 |
|---|---|
| 网络层 | 云厂商高防 IP / 游戏盾(腾讯云、阿里云都有游戏专用防护) |
| 连接层 | 连接频率限制(IP 维度)、最大连接数限制、握手需要验证 |
| 协议层 | 非法包直接丢弃,不做业务处理;限流每 IP 每秒包数 |
| 架构层 | 接入层与逻辑层分离,打接入层不影响游戏逻辑(但会挡玩家进来) |
| 运维层 | 快速拉黑名单、封 IP |
最容易被忽略的一点:接入层要能独立扩容。被打的时候多加几台接入层机器,逻辑层不受影响。
11.6 GM 权限与审计
独立开发者的系统里,GM 后台是最容易被入侵的入口(因为它能直接改数据)。
1 | 权限分级: |
这正好是你的强项。你在 Laravel + Filament 里做过的 RBAC 权限系统、FastAdmin 里的菜单/权限体系,直接搬过来就是 GM 后台。这是从 Web 转游戏服务端最容易被低估的一块价值。
审计日志要求:
- 不可篡改(只允许追加,不允许删改)
- 包含完整上下文(操作人、IP、时间、变更前后值)
- 有告警(一次改 100 万金币要立刻报警)
11.7 常见安全误区
| 误区 | 正确做法 |
|---|---|
| “客户端在局域网就放松校验” | 永远不放松。公司内网也是不可信的 |
| “加了签名就安全了” | 签名防重放,业务校验才是根本 |
| “数值在客户端算,加密就行” | 加密协议 ≠ 安全。客户端有内存可读 |
| “GM 后台加个登录密码就行” | 要 2FA + 操作审计 + IP 白名单 |
| “上线前不需要做安全” | 上线前是最容易改的窗口,那时候做成本最低 |
第 12 章 学习路径:一份 12 周的实战路线图
这份路线的设计原则:
- 每阶段都有可运行的产物,不是”学完再做”
- 越早接触难点越好——同步模型、tick 循环、AOI 不能拖到最后
- 复用你的存量优势——数据库、Redis、后台、权限,这些不用重学
- 不追求”学完”,追求”能上线一个小游戏”
12.1 第 1-2 周:建立手感(Go + 游戏循环)
目标:跑通一个能玩的”多人聊天室 + 小游戏”
技术:Go + TCP/WebSocket + Protobuf
1 | 第 1 周: |
产物:一个能用浏览器打开的多人小方块移动 demo
这一周要意识到的三件事:
- PHP 的
while (true) { sleep(1) }在 Go 里是for { select { ... } } - 每个连接一个 goroutine 是最简单的模型(不需要 NIO)
- tick 循环和事件处理是分离的两件事——网络消息进队列,tick 时统一处理
12.2 第 3-4 周:状态同步实战
目标:做一个真正像游戏的场景同步
1 | 第 3 周: |
产物:一个 50 人同屏的 2D 小游戏 demo
关键指标:打开 DevTools Network 面板看单包大小。这是你第一次直观感受带宽。
12.3 第 5-6 周:数据层与配置化
1 | 第 5 周: |
产物:一个能登录、存数据、改配表不用重启的游戏
这一步是你最大的舒适区——背包、任务、配置、数据库,全是 Laravel 里的老朋友。你在这里能重建自信。
12.4 第 7-8 周:房间、匹配、运营闭环
1 | 第 7 周: |
产物:一个能运营的闭环(登录→匹配→对战→结算→排行榜→GM 改数据)
第 8 周是最爽的一周。GM 后台你做过 FastAdmin 和 Filament,一天就能搭出来。这是你在转游戏服务端路上最大的正反馈点,一定要走到这里。
12.5 第 9-10 周:客户端(如果需要)
1 | 第 9 周: |
如果客户端用 Unity 且你想走 ET 路线:这里要重新评估,因为 ET 的价值就在于双端共享。第 9-10 周会变成”用 ET 重构前面的代码”——但这个重构成本是值得的,因为之后每次改动都是双端一起改。
12.6 第 11-12 周:上线准备
1 | 第 11 周: |
产物:一个真实可访问、有玩家能玩的游戏
12.7 12 周之后的持续成长
1 | 第 13-24 周:把游戏做深 |
之后的方向选择:
| 方向 | 内容 |
|---|---|
| 做深玩法 | 战斗系统、成长线、社交 |
| 做大规模 | 分服、跨服、集群、CDN、全球部署 |
| 做工具链 | 自研编辑器、策划工具、数据分析平台 |
| 做平台化 | 把你的游戏服务端框架做成产品(面向其他开发者) |
以你现在的技能结构,我建议走”做深玩法 + 工具链”。工具链(编辑器、配表工具、GM 平台、运营系统)是你 Web 背景的差异化优势,别人做不了。
第 13 章 推荐技术栈清单(分三档)
13.1 档位 A:「快速验证」——最快做出能玩的东西
适合:第一个项目、验证玩法、2D 休闲/卡牌
1 | 客户端 Cocos Creator (TypeScript) / 纯 Canvas |
优势:一周能出原型,同语言(TS)省掉跨语言协议成本
劣势:CPU 密集逻辑有 GC 天花板,不适合重度战斗
13.2 档位 B:「标准商业」—— 推荐主推 ⭐
适合:MMORPG / SLG / 卡牌 / 休闲竞技,能运营几年
1 | 客户端 Unity (C#) 或 Cocos Creator (TS) |
13.3 档位 C:「重度 / 双端一体」
适合:Unity 客户端 + 中重度逻辑(ARPG/MMO),追求极致效率
1 | 客户端 Unity 6 (C#) |
优势:双端一份逻辑代码,省掉 30-50% 的逻辑工作量;单步调试分布式逻辑
劣势:C# 技术栈在游戏外的就业面窄;ET 文档以中文为主
13.4 三档对比
| 维度 | A:Node.js | B:Go ⭐ | C:C#/ET |
|---|---|---|---|
| 上手速度 | 最快 | 快 | 中 |
| 性能上限 | 中 | 高 | 高 |
| 双端共享 | ✗ | ✗ | ✓ |
| 生态成熟度 | 游戏侧中 | 游戏侧中高 | 游戏侧中高 |
| AI 辅助质量 | 高 | 最高 | 高 |
| 单人可维护性 | 高 | 高 | 中 |
| 适合的项目规模 | 小 | 中大 | 中大 |
13.5 如果要我给你一句话建议
先用 Node.js + Colyseus 花一周做一个能玩的小 demo,确认你真的喜欢这件事(这是最重要的一步,很多人写完服务端才发现自己不喜欢)。
然后正式项目直接上 Go,从第一天就按 B 档的架构走,物理上先单机、逻辑上分好层。
如果客户端确定是 Unity 且逻辑重度,第 8 周时认真评估 ET——双端共享代码的价值在项目后期会越来越大。
第 14 章 学习资源清单
14.1 必读书籍
| 书 | 价值 | 优先级 |
|---|---|---|
| 《游戏编程模式》(Game Programming Patterns) | 游戏架构的通用语言:状态模式、享元、对象池、事件、组件、ECS。无论用什么语言都该读 | ⭐⭐⭐⭐⭐ |
| 《Networked Physics》(Daniel Raknet 等) | 网络同步的深度参考 | ⭐⭐⭐⭐ |
| 《Serverless Game Server》/ 《Game Server Architecture》 | 服务端架构模式 | ⭐⭐⭐ |
| 《大型网站技术架构》/ 《凤凰架构》 | 你的存量知识,直接迁移 | ⭐⭐⭐⭐ |
| 《Redis 设计与实现》 | 你的存量知识 | ⭐⭐⭐⭐ |
14.2 必读源码/仓库
| 项目 | 语言 | 为什么读 | 优先级 |
|---|---|---|---|
| skynet | C+Lua | 中文游戏服务端的事实标准;云风的十字链表 AOI;Actor 模型 | ⭐⭐⭐⭐⭐ |
| Pitaya | Go | 现代 Go 游戏服务端的完整形态;connector/handler 分离 | ⭐⭐⭐⭐⭐ |
| ET Framework | C# | 分布式游戏服务的工程化极致 | ⭐⭐⭐⭐⭐ |
| Colyseus | TypeScript | 房间抽象、声明式状态同步的最佳实践 | ⭐⭐⭐⭐ |
| Nakama | Go | 全栈游戏后端(认证/存储/匹配/聊天) | ⭐⭐⭐⭐ |
| Luban | C# | 配表流水线 | ⭐⭐⭐⭐ |
| Colyseus / Photon 对比实现 | — | 看同一问题的不同解法 | ⭐⭐⭐ |
| awesome-gameserver-cn | — | 框架索引,找资料用 | ⭐⭐⭐ |
| KBEngine | Python | 成熟的 MMO 引擎,AOI/watcher/witness 体系 | ⭐⭐⭐ |
| Pomelo(已停更) | Node | 设计理念经典(房间/组件/handler) | ⭐⭐ |
读源码的方法:不要从头读。挑一个具体问题(比如”AOI 的 enter/leave 怎么算的”),在仓库里搜相关文件,读懂那一段。
14.3 文档与规范
| 资源 | 用途 |
|---|---|
| Protocol Buffers 官方文档 | 编码规范、兼容性规则、best practices |
| FlatBuffers GitHub | 零拷贝原理 |
| Unity Netcode 官方文档 + Multiplayer Samples | NGO 的权威用法 |
| FishNet 文档 + Discord | 预测/校正的实操 |
| Agones 官方文档(Google) | 专用服务器编排 |
| Open Match 官方文档 | 匹配服务 |
| Nakama 文档 | 全栈后端 |
| Colyseus 官方文档 + Discord | 房间/状态同步 |
| Luban 官方文档 (datable.cn) | 配表 |
| Google SRE Book(免费在线) | 必读,游戏服务端本质上是分布式系统,SRE 的方法论完全适用 |
| Game Programming Patterns 英文在线版(免费) | 模式速查 |
14.4 持续跟踪的信息源
| 来源 | 内容 |
|---|---|
| GameDevNation / GameDev Report | 行业动态 |
| GDC Vault(免费部分) | 顶级技术分享(Netcode 专场信息量极大) |
| Unity / Unreal 官方博客 | 引擎与网络方案更新 |
| Nakama / Agones / Colyseus 的 Discord | 实战问题 |
| awesome-gameserver-cn | 中文框架索引 |
| 知乎 / 公众号「游戏行业」相关 | 国内实践 |
14.5 练手项目建议(按难度递增)
| 难度 | 项目 | 练到什么 |
|---|---|---|
| ⭐ | 多人聊天室(Go + WS) | 长连接、协议、心跳 |
| ⭐⭐ | 2D 小方块多人游戏 | 状态同步、插值、tick |
| ⭐⭐ | 带 AOI 的 2D MMO 房间 | AOI、降频、带宽控制 |
| ⭐⭐⭐ | 卡牌对战服务器 | 房间、匹配、回合制逻辑、结算 |
| ⭐⭐⭐ | 休闲竞技 + 匹配 + 排行榜 | 完整闭环:匹配、Elo、ZSET |
| ⭐⭐⭐⭐ | MMO 房间 + GM 后台 + 配表 | 商业化项目的完整形态 |
| ⭐⭐⭐⭐⭐ | 帧同步 Demo | 确定性定点数、状态快照、回滚 |
第 4-5 个练手项目基本可以变成你的正式项目。不要每个都做完——做完第 3 个就该考虑做自己的产品了。
第 15 章 常见坑清单
15.1 架构层
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 过早分布式 | 一个人维护 10 个服务,累死 | 逻辑分层、物理单机起步 |
| 2 | 每个房间一个进程 | 内存浪费、进程数爆炸 | 一个进程管 N 个房间 |
| 3 | 有状态服无状态扩 | 玩家随机漂移、状态丢失 | 一致性哈希 + 状态外置 |
| 4 | 所有逻辑塞一个服务 | 改一个活动要全量发布 | 按变更频率分层 |
| 5 | DBProxy 只是个 JDBC 封装 | 缓存、路由、分片没做 | 承担真正的数据层职责 |
| 6 | 分布式 ID 上线后改 | 数据全乱 | 立项定好,永不改 |
| 7 | 没有 traceID | 出问题查不出链路 | 消息头带 traceID |
15.2 性能层
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 8 | 每包都打日志 | 磁盘打满、服务崩溃 | 采样 + 计数 |
| 9 | 每帧分配对象 | GC 压力、帧率抖动 | 对象池 |
| 10 | 主逻辑里同步查库 | 所有玩家一起卡 | 内存态 + 异步落库 |
| 11 | 广播给所有人 | 带宽爆炸 | AOI |
| 12 | ulimit -n 默认 1024 | 连接数上不去就断了 | 调到 10 万+ |
| 13 | 慢客户端不踢 | 一个客户端拖垮全房间 | 发送队列上限 + 主动断开 |
| 14 | 在 tick 里做 JSON 序列化 | 火焰图第一名的常客 | Protobuf / 预编码 |
| 15 | 不看 P99 只看平均 | 隐藏的长尾问题 | 强制看分位数 |
15.3 同步层
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 16 | 帧同步里用浮点 | 不同步 | 定点数 |
| 17 | 帧同步里用 Time.deltaTime |
不同步 | 固定逻辑帧常量 |
| 18 | 遍历 Dictionary | 顺序不确定 → 不同步 | 有序容器 |
| 19 | 忘了 isLocalPlayer 检查 |
所有客户端控制所有玩家 | 第一个多人项目最常见的 bug |
| 20 | 本地玩家也用插值 | 操作有延迟感 | 本地预测,远程插值 |
| 21 | 回滚逻辑里有外部副作用 | 重演时重复发奖 | 副作用分离到 tick 之外 |
| 22 | 没有 checksum | desync 无法定位 | 定期状态 hash 比对 |
| 23 | AOI 边界抖动 | enter/leave 事件风暴 | 滞回(进入/离开阈值错开) |
| 24 | 没做延迟模拟测试 | 真实网络下崩溃 | 从第一天注入延迟/丢包 |
15.4 数据层
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 25 | 支付回调不幂等 | 重复发货 → 资损 | 唯一索引 + 幂等键 |
| 26 | 信任渠道传来的商品信息 | 玩家改金额 | 以自己系统的订单为准 |
| 27 | 金额用 float | 精度丢失 | 整数(分) |
| 28 | 配置表无校验 | 线上才发现引用了不存在的 ID | Luban 的 ref 检查 |
| 29 | 客户端和服务端配置版本不一致 | 显示和实际不符 | 握手时交换版本号 + 告警 |
| 30 | DB 直接改玩家数据 | 缓存不一致 | 走服务端的 GM 接口 |
| 31 | 无数据备份 / 无恢复演练 | 事故时数据没了 | 自动备份 + 定期演练恢复 |
15.5 运维层
| # | 坑 | 后果 | 正确做法 |
|---|---|---|---|
| 32 | 不处理 SIGTERM | 容器被杀,玩家数据丢 | 优雅退出 + 落盘 |
| 33 | 磁盘满不告警 | 半夜服务崩溃 | 磁盘告警 + 日志轮转 |
| 34 | Nginx 忘配 Connection "upgrade" |
WebSocket 握手 400 | 照抄配置 |
| 35 | proxy_read_timeout 太短 |
长连接被掐断 | 调大 + 心跳间隔对齐 |
| 36 | 不透明真实 IP | IP 限流失效 | X-Real-IP + X-Forwarded-For |
| 37 | 没有灰度 | 出问题全量受影响 | 灰度 + 快速回滚 |
| 38 | DB 变更锁表 | 服务停摆 | 在线 DDL + 向前兼容 |
| 39 | 没有压测就上线 | 开服就崩 | 上线前必压 |
| 40 | GM 后台权限过大 | 内部事故 | RBAC + 审计 + 告警 |
附录 A:从 PHP 到游戏服务端的能力映射表
| PHP 技能 | 游戏服务端对应 | 迁移难度 | 备注 |
|---|---|---|---|
| Laravel 路由/控制器 | 消息号路由(opcode) | 低 | 从 URL 路由换成消息分发 |
| Eloquent ORM | 手写 SQL 或 sqlx/GORM | 低 | 玩家数据不适合 ORM,用仓储模式 |
| Redis 缓存 | Redis 热数据 | 无 | 直接用 |
| Redis ZSET 排行榜 | 排行榜 | 无 | 直接用 |
| MySQL 表设计 | 玩家/物品/订单表 | 无 | 直接用 |
| 数据库迁移 | 数据结构变更 | 低 | 加”向前兼容”约束 |
| 队列 / 事件 | 消息队列、事件总线 | 低 | |
| 定时任务 | 日常任务、结算 | 低 | |
| 缓存锁 | 分布式锁 | 低 | 但要理解它的失效场景 |
| 支付回调 / 幂等 | 充值发货 | 无 | 你的最强项 |
| RBAC / 权限 | GM 后台权限 | 无 | 你的最强项 |
| Filament / FastAdmin 后台 | GM 平台、运营后台 | 无 | 你的最强项 |
| 导表 Excel 脚本 | Luban 配表流水线 | 低 | 加类型系统和校验 |
| Nginx 配置 | 反向代理 WebSocket | 低 | 加 Upgrade 头 |
| Docker | 容器化部署 | 无 | 直接用 |
| Supervisor / systemd | 进程管理 | 无 | 直接用 |
| Prometheus / Grafana | 监控 | 低 | 加游戏专属指标 |
| 限流 / 防重 | 游戏风控 | 低 | |
| 分布式 ID(雪花) | 玩家 ID、物品 UID | 无 | 直接用 |
| 分布式事务 | 不适用 | — | 改用最终一致 + 补偿 |
| 同步阻塞 IO | 不适用 | — | 全部改成异步/内存态 |
| 协程(Swoole) | goroutine | 中 | 概念相通,实现不同 |
| RESTful API | 消息协议 | 中 | |
| 单元测试 | 战斗逻辑测试 | 低 | 比 Web 更重要 |
| 消息队列消费者 | 落库 / 跨服 | 低 |
能力映射总结:
- 约 60% 的技能可以直接迁移(数据层、后台、运维、支付)
- 约 25% 需要改造(路由、协程、队列、定时任务)
- 约 15% 是全新的(同步模型、AOI、tick 循环、帧确定性、战斗系统)
这 15% 就是你转型要学的东西——而它们恰好是游戏服务端最有价值的部分。
附录 B:技术选型决策速查(贴在显示器上)
1 | ┌─────────────────────────────────────────────────────────────┐ |