客户端上报不可信:游戏服务端数据校验与对局重演设计

最近在整理一套游戏结算链路时,我重新看了下客户端上报的数据结构。接口已经有签名,奖励也放到了服务器计算,但修改攻击力、变速和无敌之类的异常对局,仍然可能正常结算。

问题不在于有没有服务端,而在于服务端究竟掌握了多少判定依据。如果伤害、击杀和通关结果仍由客户端给出,服务器只是把它们换算成奖励,核心规则实际上还在客户端手里。

一、先区分三类问题

客户端数据异常大致可以分成三层:

  1. 请求被直接修改或重放:金币从500改成50000,同一个结算包提交多次;
  2. 战斗过程按错误规则运行:攻击力被改高、本地时间被加速、扣血逻辑被绕过;
  3. 数据完全合法,但操作来自脚本或宏:单局看不出冲突,长期行为不像真人。

签名、时间戳和nonce主要处理第一层,服务端状态校验与对局重演处理第二层。第三层已经超出单局数据校验的边界,需要跨局行为分析。

二、事件协议只接受操作意图

开局时由服务器创建battleId,同时保存角色属性、关卡版本和随机种子。客户端后续上报关键操作,最小事件可以设计为:

csharp 复制代码
public record BattleEvent(
    long BattleId,
    int Seq,
    int ClientTick,
    short Action,
    long TargetId,
    ulong CheckpointHash);

各字段的职责要分开:

  • battleId:确认事件属于哪一局;
  • seq:检查乱序、缺失和重复提交;
  • clientTick:结合心跳判断本地时间漂移;
  • action、targetId:描述玩家操作,而不是客户端裁决结果;
  • checkpointHash:辅助定位状态差异,不能用来证明客户端可信。

服务端还要自行记录recvTime。客户端传来的时间只能作为观察值,不能单独用于冷却、限时奖励等关键判定。

这里最容易犯的错误,是让客户端上报"造成10000点伤害"或者"目标已死亡"。即使奖励在服务端计算,输入仍然是客户端给出的结论。这种处理只是把信任点从reward移到了events。

更稳妥的方式是让客户端只报操作:

csharp 复制代码
foreach (var e in events.OrderBy(x => x.Seq))
    ApplyIntent(context, e.Action, e.TargetId);

Grant(playerId, CalculateReward(context.VerifiedResult));

命中、伤害、冷却、掉落和奖励由服务器根据战前快照、关卡配置和随机种子推导。无法完整服务端化的项目,也应优先收回货币、稀有道具、首通奖励和排行榜积分的决定权。

三、校验顺序要从确定性冲突开始

服务端收到结算数据后,可以按下面的顺序处理:

  1. 校验账号、角色与battleId的绑定关系;
  2. 检查对局状态,已经结算的请求直接按幂等结果返回;
  3. 检查seq是否重复、倒退或出现异常缺口;
  4. 校验事件值域与当前服务器状态;
  5. 执行冷却、能量、血量和库存等过程约束;
  6. 根据风险分数决定发奖、延迟发奖或完整重演。

非法battleId和重复结算属于确定性冲突,没必要进入风险累计。这样既能减少后续计算,也方便区分协议错误和作弊风险。

四、冷却检查不能只看相邻事件

以一个冷却8秒的技能为例,如果10秒内释放5次,按时间窗口检查会比只比较相邻事件更稳定。n次释放至少需要覆盖(n-1) × CooldownMs:

csharp 复制代码
bool broken = events.GroupBy(e => e.SkillId).Any(g => {
    var t = g.OrderBy(e => e.ObservedMs)
             .Select(e => e.ObservedMs).ToArray();
    var cd = GetEffectiveCooldown(g.Key, serverBuffs);
    return t.Length > 1 &&
           t[^1] - t[0] + jitterMs < (t.Length - 1) * cd;
});

ObservedMs应使用服务端接收时间,或者使用经过心跳锚定的客户端单调时钟。GetEffectiveCooldown必须读取服务器保存的Buff、充能和冷却重置状态,否则正常的减CD效果也会被判成异常。

同一套思路可以继续检查:

  • 技能释放前,能量是否足够;
  • 受到攻击后,血量变化能否对应;
  • 使用道具前,库存是否真实存在;
  • 击杀和通关事件,是否满足前置状态;
  • clientTick与服务端时间,是否持续按固定比例漂移。

最后一项常用于发现变速。如果本地时间持续比服务器快,而不是偶发网络抖动,就值得提高风险分数。

五、无敌为什么更难判断

无敌类修改有个前提:服务器必须知道玩家原本应该受到什么伤害。

如果敌方攻击、碰撞结果和角色状态都由客户端裁决,服务器只在结算时收到"剩余血量100",就没有足够证据证明这局是否开过无敌。增加签名字段解决不了这个问题,因为签名保护的是数据传输,不是客户端规则本身。

要校验无敌,至少要保留敌方行为、关键碰撞、血量变化或可重演的状态快照。拿不到这些数据时,系统应把它标记为能力边界,而不是用一条不可靠的规则强行判断。

六、对局重演先算成本,再决定抽样比例

不必一开始就上传每一帧。按下面这组定长字段粗略估算:

字段 字节数
battleId 8
seq 4
clientTick 4
action 2
targetId 8
checkpointHash 8
单条事件合计 34

每局记录80条关键事件,原始字段约2.7KB。实际带宽还要加上协议头、索引,并通过真实数据测试压缩效果。

CPU预算也可以分层:所有对局执行幂等、序号、值域和轻量状态校验;普通对局抽5%完整重演;风险对局100%重演;规则重演先以P95不超过2ms/局作为压测起点。这里的比例和耗时只是容量估算示例,不能直接当成通用标准。

七、风险分数要对应明确处置

规则只返回true或false,后续很难处理网络抖动和玩法例外。更实用的做法是记录异常类型并累计风险分数,例如:

text 复制代码
冷却冲突        +40
持续时钟漂移    +25
关键状态不一致  +60
宏相似度异常    +15

总分达到60时延迟发奖并进入完整重演;达到100时冻结本次结算并转入复核。权重和阈值必须根据具体玩法的误报率调整。

八、服务端校验也有边界

脚本和宏可以提交完全合法的数据:技能冷却正常,伤害由服务器计算,事件也能成功重演,只是操作不是玩家本人完成的。

这时要把观察周期拉长,检查操作间隔是否稳定得反常、路径是否长期重复、账号是否全天运行,以及多个账号是否共用相似的设备与行为模板。客户端环境检测也可以提供注入、自动化工具和异常进程等补充信号。

服务端校验能够判断一局战斗是否违反规则,不能单独证明屏幕前坐着真人。工程上更准确的划分是:接口校验处理请求真实性,状态重演处理规则一致性,跨局检测处理行为真实性。

接口评审清单

检查问题 最小落地项
请求是否属于本局 服务端下发battleId,校验nonce、seq和幂等状态
客户端报的是操作还是结果 只接收操作意图,关键结果由服务器计算
战斗过程能否成立 校验冷却、能量、血量、库存和事件顺序
时间是否被加速 对比心跳锚定时间与服务端时间,并保留抖动容差
重演成本是否可控 全量轻规则、普通对局抽样、风险对局全量重演
数据正常是否等于真人操作 不等于,脚本和宏进入跨局行为检测
相关推荐
字节暗面1 小时前
游戏服务端如何校验客户端上报数据?四层校验思路详解
游戏开发
chingho2 小时前
GitHub上这10个C#项目,不仅实用,还怪好玩的
c#·github·.net·开源项目
半亩码田3 小时前
C#转Python第5.1篇:异步编程:asyncio vs async/await,语法相似但机制不同
开发语言·python·c#
阿华田5124 小时前
Java 类加载冲突实战:解决 is not an instance of
c#·linq
samble13 小时前
从零搭建灌装监控系统(十七):LiveCharts 实时温度曲线
c#·wpf·mvvm·modbus·工业控制
甲维斯16 小时前
我用AI做了一款「三国吃鸡」,开局孔明灯,落地青龙刀 !
人工智能·游戏·游戏开发
SmalBox18 小时前
04-01-源码-编辑器总览
unity3d·游戏开发
淡海水1 天前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
小羊没烦恼!1 天前
OS X 上使用.NET开发应用程序
开发语言·前端·数据库·人工智能·c#·策略模式