大型游戏服务端读书笔记
第一章 引入
服务器承载量
从CPU负载、内存占用、网络流量等多个角度考量,也需要考虑不同游戏类型的逻辑复杂度。
可以做个假设,在MMORPG中,玩家平均每3秒操作一次(走路,购物),服务器平均处理一条消息花费2毫秒。从CPU角度来看,如果同一时刻只处理一个客户端请求,服务器每秒可以处理500条消息,最高承载1500人。按经验推算,最高1000多人在线的游戏,DAU大概是三五千,这种承载量对于多数独立游戏和小型手游是足够的。
上面是仅计算了CPU负载,实际上”走路”这类广播消息受网络影响很大,不同类型游戏对服务器响应速度有不同要求。如果不做进一步优化,广播的消息量与在线玩家呈指数增长关系,通常这类单进程程序只能支持几十名玩家进行广播。
强弱交互
同一场景角色交互很强,比如有”走路”消息,可以在
同一进程处理。不同场景角色交互较弱,只有聊天、好友、公会这些功能需要交互,可以将同一个服的玩家放在
一台机器上处理,进程间通信会比同一进程共享数据慢数百倍。不同服务器的玩家交互很少,可以放到
不同机器上。
一致性问题
分布式程序要处理断线重连、断线期间的消息重发,以及 断线后进程间状态不一致的问题。
在游戏业务中,开发者一般会把一致性问题抛给具体业务去处理。
分布式与单点
难以分割的业务
实现分布式程序的前提是游戏逻辑能够分割。
如果游戏规则复杂,各个功能紧密相连,则不容易找到分割的方案,部分功能依然要靠单点的性能支撑,那么单点(单个进程、单个线程)的运算能力依然会限制服务端的承载量。
延迟和承载的权衡
多个程序协作意味着消息延迟,某些功能对消息即时性要求很高,比如帧同步。
Actor模型
合理分割功能是分布式模型一大难点,Actor模型既能符合游戏逻辑的表达,又能让计算机高效执行。
Actor模型中,每个Actor相互隔离,只通过消息通信,具有天然的并发性。理念是万物皆Actor,它是更进一步的面向对象,即把世间万物都当作Actor对象。Actor可以代表一个角色、一只动物,也可以代表整个游戏场景。
每个Actor都会包含自身状态(Items,HP),以及一个信箱(消息队列),Actor通过给其他Actor寄信来实现通信。至于收到信件后的反应,取决于收信Actor。
第二章 Skynet入门
一些API
目录结构,Service即Actor,放在cservice和service;lib库放在luaclib和lualib。
1 | -- 启动一个新服务 |
1 | -- 返回 listenfd |
1 | -- 连接数据库 |
1 | -- 本节点(重新)加载节点配置 |
协程同步问题
服务当前协程挂起时,还可以接受并处理其他消息,如果多个协程改到同一份数据,就会有同步问题。
解决方案,加多一个state标识和一个协程列表,操作执行时,将state置doing,其他协程判断state=doing时就将自己加到协程列表,然后skynet.wait。在操作执行完后,重置state,然后遍历协程列表依次skynet.wakeup(co),最后将协程列表置空。
第n章 进阶用法
封装易用的API
Skynet的API提供了偏底层的功能,不方便使用,通过snax框架给出了一套更简单的API。
本节在service模块封装了更简洁的API,service模块是对Skynet服务的一种封装,还封装了重复调用的方法。
1 | local M = { |
分布式登录流程
解决以下问题
角色Actor的创建和销毁
SSO,登录和断线重连时检查,注意同一个角色在不同类型节点都只能有一个服务协程
利用agentmgr服务裁决登录请求,根据玩家在线状态(登录中、已在线、登出中)避免临界情况
扩展单点agentmgr服务,利用对ID取模路由到多个agentmgr服务,避免竞争
游戏数据库
避免扩展数据库结构导致长时间停服,需要保证数据库结构稳定。可以采用Key-Value,将玩家数据序列化成二进制数据BLOB
Protobuf对比Json,序列化数据小,能设置默认值(proto3不支持)
分库分表,分表依据有数据更新频率,数据更新时机以及数据量大小
同步算法
游戏特征
fps游戏,精确度要求高,一局玩家数量多,同屏角色数量少
rts游戏,同屏单位数量多,一局玩家数量不多
延迟和抖动
“顿挫”和“打不中”都可以归结于网络的延迟和抖动,即使服务器设置很高的同步频率,也无法解决
TCP解决了UDP不可靠和无序的问题
MMO玩家能容忍0.1秒的延迟,MOBA玩家的容忍程度很低
客户端障眼法
插值算法
对比直接设置位置,存在更大的误差,但带来更好的效果
缓存队列
收到移动协议,不立即处理,而是把数据缓存在队列里,以固定的频率取出,结合插值算法移动角色,缓解网络抖动
根据缓存队列积累的数据量,动态调整取出速率
对比插值算法,误差更大
主动方优先
对于误差敏感的游戏(例如射击),如果游戏服不具备完全的运算能力,选择优先照顾主动方感受,应对误差
同步方案
根据服务器的输入输出内容,同步方案分为三类(输入 -> 输出):
- 指令 -> 指令,帧同步的基础
- 指令 -> 状态,杜绝作弊,服务器工作量大,客户端可以先行表现
- 状态 -> 状态
指令例如移动操作,状态例如坐标位置。选择方案需要从开发难度、人员配置、服务器硬件水平等多方面考量。
可以结合多种同步方案,例如,某些ARPG使用 状态->状态 的方案同步角色的位置,使用 指令->状态 的方案同步技能,使服务端具备一定的反作弊能力,同时又能平衡工作量。因为客户端引擎大多集成了物理模块和寻路模块,容易实现角色移动的功能;服务端实现起来则相对比较困难,也增加了服务器负载
MMO 天刀
场景部分采用 状态 -> 状态 的同步方案,辅之以服务器强校验。该游戏的场景结构、技能触发规则都很复杂,完全由服务端运算,负载太高,所以服务器用低精度的地图数据和较低的运算频率来降低负载
MOBA 王者
一般采用帧同步,误差较小来保证公平性,方便处理同步单位较多的情况,容易实现战场回放。帧同步对网络质量的要求很高(延迟要求低于50ms),为降低延迟,采用自研的可靠UDP。帧同步需要全局计算
FPS
一般采用状态同步,延迟低(无需服务器计算),更依赖客户端算力(服务器出于性能考虑,采用低精度地图,而fps对物理碰撞精度要求很高)。客户端只关注玩家周围事物,计算量小(帧同步需要全局计算),很难防止外挂
帧同步
在 指令 -> 指令 方案的基础上,增加了一些用于确保不同客户端能有相同运算结果的机制(确定性计算),再配合客户端的障眼法、可靠UDP等技术,为玩家提供良好的游戏体验。
严格帧同步,客户端之间的误差不会超过一轮。乐观帧同步,公网环境下采用定时不等待策略,服务器定期广播操作指令,推进游戏进程,不必等待慢客户端。广播策略简单,但是收集策略复杂。可以设定最大允许的轮数差异
确定性计算
浮点数计算,不同系统浮点数精度客户端不同,可以全部转成整数计算
随机数,客户端使用同一套伪随机算法,服务器同步同一个随机种子
遍历顺序,避免使用foreach语句的遍历,保证顺序
多线程、异步、协程
AOI 算法
九宫格算法
不仅减少广播量,也减少了物理碰撞的计算量
可靠UDP
TCP即使禁用Nagle算法,也很难达到延迟在50ms以内的要求
通信的三角制约
低延迟、低带宽、可靠性三者不可兼得,TCP牺牲了低延迟,UDP牺牲了可靠性
在网费降低的背景下,低带宽成为可以权衡的因素
低延迟、可靠性、高带宽的通信协议更符合游戏的需求,较普遍的做法是在UDP基础上,新增一些可靠的机制(例如多发几次,降低丢失概率;增加确认重传机制)
防外挂
不信任客户端
服务端向客户端发送的信息越多,外挂有机可乘的可能性就越大
fps游戏,服务器很难精准获悉玩家的视野范围,只能向客户端多发送一些冗余信息
防外挂常用措施
防变速挂
由于加速器改变的是全局时间,因此其也会改变心跳包的发送频率,从而露出马脚
防封包工具
防止重发攻击,增加校验码
帧同步投票
外挂的根源是游戏对客户端算力的依赖,帧同步很容易作弊,服务端可以通过投票机制找出作弊的玩家
服务端可以要求每个客户端每隔一定的帧数就发送一次状态协议,协议中包含客户端当前的帧数及状态码。如果没有作弊,那么在同一帧时,各客户端应处于同样的状态,状态码也应相同
防外挂的核心要点,就是要尽可能多地让服务端做逻辑运算、尽可能多地校验客户端的运算结果,不要相信客户端的一切输入
终章
分层架构
把服务端的所有模块分为几个层,并规定每个层的模块只能调用平级或者下一级的模块
实际项目中,有些逻辑很复杂,难以百分百遵循分层规则
| —- | —- | —- | —- | —- | —- | —- | —- |
| 业务层2 | 公告 | 签到 | 邮件系统 | 排行榜 | 战斗系统 |
| 业务层1 | 道具模块 | 地图 | 角色数据 | 离线消息 | 技能 | Buff | AOI |
| 框架层 | 网络模块 | 调度模块 | 节点通信 | 登录模块 | 热更新模块 | 数据库模块 |
| 底层 | 数学库 | 字符串库 | 缓冲区数据结构 | 编码解码方法 |
代码地址
https://github.com/luopeiyu/million_game_server