动作手游实时PVP帧同步方案

动作手游实时PVP帧同步方案(客户端)

1、概述

1.1、基于UDP的帧同步方案

  在技术选型方面,之所以选择帧同步方案,在Kevin的一篇介绍PVP帧同步后台实现的文章中已经做了详细叙述,这里简单摘要如下:

  高一致性。如果每一帧的输入都同步了,在同样的上下文中,计算得出的结果应该也是同步的。

  低流量消耗。除了帧同步,其它方案(比如状态同步)想做到高一致性,需要同步非常大量的数据。无论是对于移动网络,还是固络都是不合适的。

  服务器逻辑简化。采用帧同步方案,服务器只需要做简单的帧同步,不需要关心太多的业务细节。有利于客户端功能的扩展和服务器的稳定和性能。

  反作弊。客户端只需要在适当机时上报校验数据给服务器,服务器对2个客户端上报的数据进行对比,就可以快速识别是否有人作弊。然后通过无收益的方式间接防止作弊。

  那么,为什么选择UDP而不是TCP呢?主要有2点原因:

  弱网络环境。

  实时性要求。

  我们通过一个测试APP,在WIFI4G环境下,采用TCPUDP两种方式连接同一个服务器,分别获得对应的RTT进行对比。

 

 

  我们可以发现,在弱网络环境下,UDPRTT几乎不受影响。而TCPRTT波动比较大,特别是受丢包率影响比较明显。

 

1.2、基于UDPFSP协议栈

  由于UDP具有不可靠性,所以在UDP的基础上实现一个自定义的协议栈:FSP,即FrameSyncProtocol

FSP的基本原理就是防照TCPACK/SEQ重传机制,实现了传输的可靠性,同时还采用冗余换速度的方式,又保证了传输的**速率。在帧同步方案中一举两得。

 

2、技术原理

2.1、帧同步技术原理

  如下图所示,客户端A的操作A1与客户端B的操作B1封装成OperateCmd数据发送给PVP服务器。PVP服务器每66MS产生一个逻辑帧,在该帧所在时间段内收到A1B1后,生成一个Frame数据块,在该帧时间结束时,将Frame发送给客户端ABFrame数据块内有该帧的帧号。客户端AB收到Frame数据后,便知道该帧内,客户端AB都做了什么操作。然后根据收到的操作A1B1进行游戏表现,最终呈现给玩家AB的结果是一致的。从而实现客户端AB的数据同步。

 

1 帧同步技术原理

 

2.2FSP协议栈原理

  如下图所示,发送者维持一个发送队列,对每一次发送进行编号。每一次发送时,会将待发送的数据写入队列。然后将队列里的数据+编号发送给接收者。

  接收者收到数据后,会将该编号回送给发送者以确认。发送者收到确认编号后,会将该编号对应的数据包从队列中删除,否则该数据仍保存在发送队列中。

  下次发送时,会有新的数据进入队列。然后将队列中的数据+最新的编号发送给接收者。以此循环反复。

 

2 FSP协议栈原理

 

上图解析:

  第1次发送,在发送队列里只有Data1,于是将Data1和编号1Seq=1)发送给接收者。收到确认编号1Ack=1)后,将Data1从队列中删除。

  第47次发送,由于从第4次发送开始就没有收到确认编号,于是队列中包含了Data4Data7。第7次发送后,收到确认编号6,于是将Data4Data6从队列中删除。

  第8次发送,队列中包含Data7Data8。发送后收到确认编号8,从而将Data7Data8从队列中删除。

  以上的关键点是,发送者未收到确认编号,并不一直等待,而是会继续下一次发送。结合图1

  如果发送者是服务器,则会每隔66MS会将一个Frame数据写入发送队列,然后将该队列里的所有Frame数据一起发送给客户端

  如果发送者是客户端,则会在玩家有操作时,将玩家的每一个OperateCmd数据写入发送队列,然后将该队列里的所有OperateCmd数据一起发送给服务器。如果发送队列不为空,则每隔99MS重复发送。如果发送队列为空,则不再发送。直到玩家下一次操作。

  由于服务器和客户端即是发送者,又是接收者。则服务器和客户端的每一次发送,除了会带上该次发送的编号,还会带上对对方发送编号的确认

 

3、技术实现

3.1、整体框架

 

3 PVP通讯模块整体框架

 

  这是一个典型的手游PVP通讯模块的整体框架。这里主要分享一下FSP模块和帧同步模块的技术实现。

 

3.2FSP模块

FSP模块主要用来实现FSP协议栈。其协议格式定义如下。

FSP上行协议定义:

Seq

Ack

SomeData

OperateCmd List

CheckSum

FSP下行协议定义:

Seq

Ack

Frame List

CheckSum

  如下图所示,是FSP模块的接收逻辑流程。


 4 FSP模块接收逻辑流程

 

  其中关键点是:

  对Recv New Ack判断,对曾经发送过的Operate进行确认删除。

  对Recv New Seq判断,过滤掉因为网络问题造成乱序的包。

  上图中,接收到的Frame最终都存储在RecvQueue中。我们将接收逻辑放在子线程中。所以只需要在主线程中需要Recv的时刻从RecvQueue中读取FremeList即可。

  如下图所示,是FSP模块的发送逻辑流程。发送逻辑同样放在子线程中。发送逻辑有2种触发方式:

  业务层主动调用发送

  每隔指定时间触发一次(在WIFI4G下使用不同的时间,可以减少服务器收到的纯确认包比例,有利于提高通讯性能)


 5 FSP模块主动发送逻辑流程

 

6 FSP模块定时发送逻辑流程

 

3.3、帧同步模块

  下图是帧同步模块的实现框架。

 

7 帧同步模块实现框架

 

  按照上图箭头编号描述如下:

  (1)负责接收来自FSP模块的FrameList

  (2)将FrameList里的每1帧都存入FrameQueue

  (3)同时将FrameList的每1帧的帧号进行变换后,得到客户端帧号。同时,在等下1个服务器帧到来之前,需要将客户端的帧锁定在下1个服务器帧的前一帧(LockFrameIndex)。然后 将FrameIndexLockFrameIndex传入FrameBuffer

  (4)客户端每1帧从FrameBuffer中取出当前可能需要跳帧加速的倍数(SpeedUpTimes)。

  (5)如果SpeedUpTimes0,则表示正在缓冲中,没有需要处理的帧。如果SpeedUpTimes1,则表示缓冲结束,但是不需要加速,只需要处理最新的1帧。如果SpeedUpTimes大于1,则从FrameQueue里取出这SpeedUpTimes个帧,将里面的SyncCmd取出来。

  (6)将SyncCmd传入OperationExecutor

  (7OperationExecutor与具体游戏的业务逻辑相关联,负责将SyncCmd传入给业务逻辑和预表现模块进行具体的处理。

  其流程图如下:

 

8 帧同步逻辑流程1

 

9 帧同步逻辑流程2

 

4、最新优化

4.1、断线重连优化

  在传统网络模块开发思想中,当发送超时达到阀值,或者底层判定断开连接时,需要重新建立连接。之前这部分工作是交给一个偏上层的模块来执行,该模块需要等Apollo通讯模块连接成功之后,才进行PVP通讯模块的连接。这样使逻辑变得复杂。

  由于UDP本身的不可靠性,可以认为网络断线也是其不可靠性的一部分。

  而FSP协议栈就是为了解决UDP的不可靠性而设计的,所以也附带解决了断线重连问题。

  去除了原来的断线重连逻辑之后,用FSP模块本身的特性来处理断线重连,实测能够提高网络恢复的响应速度。由于PVP服务器设定的超时阀值是15秒,有些时候,其实网络已经恢复,但是由于Apollo通讯模块对网络的恢复响应过于迟钝,造成不必要的判输。

 

4.2、接入GSDK

  从目前接入GSDK后的数据来看,能够减少一定的网络延时,但是并不明显。

 

4.3AckOnly优化

AckOnly优化是指减少服务器收到的纯确认包数据。这样做的目的是:

  减少包量,有助于在WIFI下节省路由器性能。GSDK有个统计表明,有大概20%多的网络延时是因为路由器性能造成。

  节省流量,一定程度上也可以节省网络设备性能,同时在4G下为用户省钱。

  该优化分2部分实现:

  (1)空帧免确认

  (2WIFI延迟确认

  在优化前的AckOnly比例为:57%

  空帧免确认优化后降到:38%

WIFI延迟确认优化后降到:25%

 

5、一些尝试

  将FSP模块抽象得与业务无关,使之可快速完成一个使用帧同步方案通讯的Demo成为可能。

  实验了本地局域网PVP对局,只要在同一网段下,可以成功对局。(如果有需求,可以实现该功能)

  实验了本地蓝牙PVP对局,发现蓝牙是带连接态的,并且其通讯是用类似TCP的数据流进行的。同时它与WIFI信号有干扰,如果开启WIFI,其延时非常高。在非WIFI下,其单条数据的延时很低,但是如果以66MS的频率发送数据,则延时又非常高。

  建立了一套用于FSP在线诊断和断线诊断的工具。

 

 

 

 

动作手游实时PVP技术揭密(服务器篇)

前言

  我们的游戏是一款以忍者格斗为题材的ACT游戏,其主打的玩法是PVE推图及PVP 竞技。在剧情模式中,高度还原剧情再次使不少玩家泪目。而竞技场的乐趣,伴随着赛季和各种赛事相继而来,也深受玩家喜爱,从各直播平台几万到几十万的观众可见一斑。然而,在移动端推出实时PK并不是一蹴而就的,本文将向大家介绍游戏的实时PVP相关技术。

 
技术选型

  实时PK的表现方式,是将N个玩家的行为快速同步给其它玩家展示并保持一致性的过程。这里面涉及到几个要思考的要点:

·        同步什么?可以是玩家具体操作(如移动操作),也可以是某按键操作(如方向键),这两者是有些微区别的。

·        怎么同步?可以选择方式多种,传统的C/S模式,或者是P2P形式,或者是帧同步等。

·        同步方式?载体可以是TCP/UDP。使用哪个比较靠谱?

  基于以上的考量,在游戏中,使用的是基于可靠UDP的帧同步模型作为实时PVP的技术方案。你问为什么不采用TCP,为什么不用C/S,为什么不上P2P,下文分晓。

实现细则

  这里讲述一些重要细节,以解决众多的Whynot问题。

使用帧同步模型

  为什么选择帧同步,直接原因是继承之前AI(机甲旋风)经验,对于ACT类型游戏,我们认为帧同步是不错的方案,主要是能够获得以下好处:

·        高一致性。对于格斗中的技能连招,如果不精确到帧,会出现一些诡异现象。试想某个浮空下落的角色,可能一方客户端看到已经倒地,另一方在未倒地时接上其它技能,会出现两个结果,即使将其拉扯回,同样奇怪。而帧同步的机制,保证了双方客户端的一致性。

·        服务器逻辑简化。传统的C/S 架构下,在服务器计算及校验,大量的核心逻辑需要客户端及服务器都实现一遍,使用帧同步可以大大简化及保证服务器的稳定性。

·        低流量消耗。在移动网络中,用户的流量即金钱,如果我们游戏的核心部分耗流量严重,那让45%1左右的非wifi用户情何以堪呢?

·        反作弊。讲道理来说,帧同步对于反作弊并不友好,但是有一个简单的做法可以快速反作弊,就是双方数据不一致时(上报的校验数据或者战斗结果),即检测到有人作弊。

  那么,帧同步的过程是如何进行的呢?下图演示了两个客户端PlayerA/PlayerBServer交互的过程。

  对于客户端而言,不断的上报其行为(action)至服务器,并且等待服务器下发的帧包驱动其逻辑。这种方式是帧锁定同步(lockstep)的一种演化 2
对于服务器来说,固定帧间隔(66ms)将队列中的PlayerA/PlayerBactions放在一个Frame中,并同步给两个客户端,这似乎和BucketSync类似。

  我常被问到一个问题,对方的卡顿会不会影响我的游戏体验,从以上我们的同步原理中,或许你可以找到答案。

使用UDP代替TCP

  帧同步并对协议层是TCP还是UDP并无要求,但我们打一开始就没考虑TCP直接拥抱UDP,究其原因,若是对TCP的特性稍有了解,大概会背那三字经:慢启动快重传拥塞避免(三个字!)。我概括它以下几点不太适合对实时性要求高,对延迟敏感的移动网络游戏:

·        慢启动算法不适合移动网络的情景,在移动网络环境下信号时好时坏是常态,慢启动会使数据不能及时快速达到对端。

·        拥塞避免算法不适合移动网络主要原因是其考虑到网络的公平性及收敛性,并且AIMD 算法会使实时性大受影响,延迟明显提升。

  还有TCP协议用于重传的RTO的指数变化及拥塞算法的实现Nagle的缓存等,都是TCP并不太适合高实时性要求的游戏玩法的原因,不再一一列举。

  那么为什么UDP对比TCP更合适呢?多说无益,看一组数据:

  显而易见,在各种网络情形下,UDP的表现(延迟分布)基本上都优于TCP。测试程序在相同的网络环境下,通过设置一定的延迟,丢包率,抖动等,获得TCP/UDPRTT3 

  对于P2P的选择,我们放弃的原因是本身UDP打洞并非100%成功,而若打洞失败则仍要走服务器转发,故简化设计考虑,未选择P2P方式去同步。

自己DIY可靠UDP

  上面讲了用什么(UDP)同步什么(帧数据)的问题,有同学要问了,UDP不可靠传输,丢包怎么办?当然,为了数据一致,我们不允许(或许可以允许少量)丢包,TCP的可靠性是由ack/seq+重传去保证的,世面上大多数的可靠UDP实现,也都是类似原理。

  在考察了几个可靠UDP的实现(UDTENet等)觉得略显复杂,并且在我们开发时,公司内部的可靠UDP实现未达到可使用阶段,鉴于自己重新造个轮子并不复杂,于是挽起袖子写了起来。

  用于可靠UDP的实现,其UDP协议自定义包头是这样的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

//客户端上行包

typedef struct UdpPkgRecvHead

{

    uint16_t seq; //start from 1

    uint16_t ack; //ack server seq

    uint16_t sid; //player uid

    uint8_t  action_len; //pkg actions contain

}UdpPkgRecvHead;

//...

//服务器下行包

typedef struct UdpPkgSendHead

{

    uint16_t seq; //inc when frame id ++

    uint16_t ack; //ack client pkg's seq

    uint8_t frame_len; //pkg frames contain

}UdpPkgSendHead;

 

  基于以上的定义,可靠性保证的过程大概如图如示:

  客户端在满足以下条件之一后,发包给服务器:

·        玩家有操作时

·        发包后间隔(99ms)未收到回包时

·        收到包一定间隔(99ms)后

  若玩家有操作,则确认信息随着玩家的操作上行包捎带至服务器 ; 如果无操作,则固定时间上报确认信息给服务器。客户端的seq每一个操作行为(action)时加1,服务器seq在每一帧数据下发时加1 ,并且双方的RTO 取值不同4

  对于可靠性的保证,可以采用请求重传,而我们使用的是冗余重传。使用冗余重传的一个好处是,简化了麻烦的时序问题,并且收到的每个包都是完整的顺序的。对于网络拥塞情况下的带宽利用优于TCP,它不足之处是流量略微增加了些。下图是冗余重传的过程:

  图解如下:

ClientAction1过来,记seq=1,服务器未收到。
Client又新增了Action2,此时新包将同时包含Action1,Action2,并且seq=2
Server确认了上一步骤的包,发给Client的包Ack=2表示确认。
Client由于某些原因(可能延迟等)尚未收到ServerAck=2的确认,此时新增Action3,并发包seq=3
Client再次发Action4时,发现之前已经Ack=2了,故新包将只带Action3Action4并且seq=4

  这里演示了冗余传输的过程,服务器对于收到的包,可以根据seq/ack情况动态去除冗余或者丢弃过期包。可能你会觉得全冗余是否不太合适并且有明显优化空间?在实际现网长期运行中,全冗余的冗余率是100%左右,相比于一些可靠传输的重发最近三帧等方式,这种为可靠性付出的代价是合适的并且也提高了更多实时性。

  小结:在刨除一些优化及细节外,这就是可靠UDP的机制,简单有效,开销极小5。经测试及实际线上运行来看,在弱网络环境下的表现也是不错的。

 

反作弊策略

  实现的技术细节告一段落,接下来谈谈我们的反作弊策略。有些经验是实践下的真知:)
我们知道帧同步的一切都在客户端运算了,服务器能做的显得很有限。我们不知道玩家当前的位置,不知道玩家的技能使用情况,不知道玩家当前血量,拿什么来反作弊?

实时的检测,做了两点:

  客户端固定间隔上报双方血量及技能使用情况,服务器进行记录
  单局结束,上报胜负关系

  基于这两点,我们可知道某一帧玩家的血量是多少,每个人都上报自己的及对方(们)的,双向校验可看出有有无作弊行为。困扰而不得其解的是,当只有两人时,判断谁是作弊者比较麻烦。当两人以上时,可以仲裁。

  我们做了一点容错,当胜负结果异常时,才去进一步检查上报的记录以判断作弊者,判输。而对于上报数据并不一致但是胜负关系一致的情况,记录日志来诊断(容易发生在版本变更时)问题。

  通过实时的检测,基本可以检测到单局中作弊行为(加速,无限CD,锁血等),因为他们最终都导致双方数据不一致而结算不一致或上报血量不一致。

 
非实时的统计学反作弊方案

  当有一些漏洞可被利用但是一时无法定位的时候,统计学上的反作弊会比较有用。这里说的漏洞是指通过某种行为使对方掉线或者不发包等未知原因导致游戏异常结束的行为。

  我们在游戏结算时,非正常获胜的(掉线等)都会记录下来,并且作用于一个惩罚机制。

  每天通过非正常获胜的次数有上限,达到上限后,其非正常获胜都将不计。
  非正常获胜的次数作用于实时检测逻辑,如果双方数据不一致,非正常获胜次数多的玩家失败。
  非正常获胜次数影响玩家进入匹配,次数越高需要等越久才能开始匹配。

  这个方案在线上发挥过作用,有效阻挡了少部分非正常玩家利用漏洞获益,减少了其影响面。

 
后话

  上文介绍了游戏的实时PVP的技术实现,这里配一个架构图,看看其外围。

 

 

  有两点需要说明:

·        TGW的多通接入支持UDP四层,UDP 服务需要监听所有tunelip/port

·        帧同步的原理,要求我们必须精细化匹配。游戏中是二进制版本+资源版本一致才相互匹配,可以做到更精细化的根据出战忍者双方客户端数据hash值是否一致进行匹配。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值