大型游戏服务端读书笔记
第一章 引入
服务器承载量
从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