2022-11-144905次浏览
1评论
12收藏
4点赞
分享
协议测试对于一个项目来说是非常重要的专项之一,很多线上的严重事故其实都可以通过日常的协议测试来避免。对协议测试理解的深入程度与协议测试方法的掌握程度会直接影响到最终的协议测试质量。本文将从原理介绍、工具使用、测试方法、测试推进等若干方面展开介绍协议测试相关内容,旨在帮助大家尤其是新人QA同学初步了解协议测试原理、掌握协议测试工具的使用、协议测试的具体方法以及协议测试组织与推进方法。
协议是游戏客户端与游戏服务端之间通信的载体,它约定了通讯双方数据交换的规则与标准。根据发送方的不同,协议一般区分为游戏客户端对服务端的上行协议和游戏服务端对客户端的下行协议(通常不考虑服务器与服务器之间的通信)。当收到不同协议的时候,彼此之间就会做出相应的反应。
协议被调用时,其方法的具体实现不在程序运行本地,而是在另一端(比如客户端通过协议,可以直接调用服务器的方法)。所以协议调用的过程其实是透明的,调用者只需要调用方法的最终运算结果,完全不需要关注方法实现的具体细节。
打个比方,假如小明周末在家叫了一份外卖,商家在收到小明的订单请求后就开始做外卖并配送至小明家。他无需关注商家怎么洗菜、做饭与配送,整件事需要关注的是自己通知了店家需要点外卖并耐心等待店家最终反馈的结果就可以了。
提到协议,相信各位对“RPC”(Remote Procedure Call Protocol),即远程过程调用协议,这个概念并不陌生。远程过程调用,就是调用方法并非在本地运行,而是调用在远程设备上运行的函数。RPC的本质是提供了一种轻量无感知的跨进程通信的方式,其过程是透明的。在客户端上调用服务端或在服务端上调用客户端方法与本地调用无异。其逻辑框架可大体如下图所示,其中client stub与server stub用于获取和调用远端方法。
在网易Messiah引擎中,封装了一套完整的RPC框架功能,协议就是框架下通信程序之间传递的信息数据。在我看来,每一次的协议发送就可以理解为一次RPC调用过程。那么远程调用过程是怎么做到的呢?试想,客户端希望能调用服务端对应方法,不仅仅需要ip与port找到对应game与服务端实例,更是需要定位到具体的函数。所以为了实现远程过程调用,RPC实现了三个功能:Call ID映射、序列化和反序列化与网络传输。
Call ID映射。客户端与服务端分别维护一张ID表,并且保证相同函数的ID在两张表是一样的。在实际远程调用过程中,双端是通过这个ID,来找到对应的函数的。
序列化和反序列化。由于网络协议是基于二进制的,调用方法的参数与函数名要序列化成二进制的形式才能在网络上进行传输。在实际的网络数据传输过程中,引擎中的mailbox(下文会展开解释mailbox)会在asiocore C++层将调用的函数名和参数序列化,依据mailbox远程端信息发送给远程进程上的某个玩家实体,反序列化后在玩家实体上执行这个RPC函数,完成一次RPC通信。
网络传输。即是序列化后的二进制数据传输使用的网络传输协议。TCP、UDP与KCP等协议都是可以的。
所以,整个远程过程调用的过程其实就是这样:
1. 程序从函数ID映射表中,找到要远程调用的函数的ID。
2. 然后将要调用的函数的ID,以及执行函数所需要的其他参数,进行序列化。
3. 将序列化好的数据,放进TCP或其他支持的协议中进行传输。
4. 远处的服务器程序收到后,执行反序列化,获取要调用的函数ID以及函数要用到的参数。
5. 远处的服务器按照要求调用对应的函数。
6. 如果函数有返回值的话,返回的数据也会经过序列化,然后通过TCP等协议传回给发起调用的一方。
7. 调用方获得返回的数据后,又经过反序列化,最终获取到本次远程调用的执行结果。
具体到脚本中的方法,一般通过装饰器rpc_method用于声明一个方法是否为RPC方法,并可以通过装饰器的参数判断该方法是否可以被客户端远程调用。
在Messiah引擎支持的目录结构中,脚本工程目录通过client、common和server来区分代码是运行在客户端还是服务器亦或是双端共用。 由于协议测试的关注点都是上行协议,所以我们日常需要测试的协议,理论上都存在于server与common目录中。由此看来,协议测试需要关注的协议必须满足下列所有条件:
1. 方法存在于server或common目录下的脚本中。
2. 被装饰器rpc_method修饰。
3. 参数类型为:CLIENT_ONLY(仅允许本客户端调用该方法)、CLIENT_ANY(允许任意客户端调用该方法) 与CLIENT_SERVER(允许两任一端调用该方法) 中的一种。(实际参数类型视具体项目中脚本规则而定。)
通过这种方法,就很容易实现将上行协议从脚本文件中筛选出来。如果进一步配合svn提交的hook以及svn脚本diff,就能实现对协议的日常监控,从而识别需求单是否改动到协议的。(下文会展开解释协议监控平台。)
简单叙述完了如何在脚本中判断上行协议,接下来讲下游戏内实际的调用。在我们游戏中,服务端角色可以通过@p.client.xxx来调用客户端方法,同理可以通过$p.server.xxx的方式让客户端角色脚本中调用服务端方法(@p与$p分别表示服务端avatar与客户端avatar)。
那如何做到客户端和服务器之间简单以$p.server.xxx和@p.client.xxx方式完成通讯的呢?以$p.server.xxx为例,$p.server实际上赋值了一个类(AsioServerProxy)用来作为与服务端通讯的代理对象。
在AsioServerProxy类中定义了属性查找的方法,从而实现了该代理允许以类似于本地调用的方式调用远程服务端方法。(其中call_rpc绑定了引擎封装的RPC接口)
1.def __getattr__(self, rpc_name):
2. index = send_rpc_index(rpc_name)
3. def call_rpc(self, *args, **kwargs):
4. self.callserver("", index, args, kwargs)
5. setattr(AsioServerProxy, rpc_name, call_rpc)
6. return getattr(self, rpc_name)
试想客户端实例通过$p.server.xxx在远程调用服务端方法,势必需要找到对应的服务端实例。那么在服务器集群里有若干game,它如何找到需要调用的对象在哪个game上呢?答案就是mailbox。
现实中的场景就是你要给某人发个消息,只需往他的mailbox 地址发个邮件,你的邮件会自动到达他的邮件客户端,而你不用关心他邮件客户端连的邮件服务器的ip、端口、协议都是什么。在Messiah里面是个类似概念,一个entity调用另一个entity的RPC,那么就需要知道对方的mailbox。
mailbox分为两种,一种是base_mailbox:
1.proxy = asiocore.base_mailbox(hostnum,client_id,area.id,gate_ip,gate_port)
从参数定义上我们可以很明显看出,base_mailbox记录了entity的客户端所连的gate进程的ip、port以及自己的client_id。当客户端调用服务端RPC的时候,gate收到这个RPC消息,通过其,client_id找到所在的game的 ip和game 的port,然后转发到这个game。game再根据area.id(在Messiah引擎中每个服务器实例都拥有一个area,作为在引擎各个系统中的底层对象。
同样在客户端中也有area作为客户端实例在网络层的引擎对象。)找到是哪个对象的area对象应该处理这个消息,最终交给该area对象调用对应的python RPC函数。
另一种是cell_mailbox:
1.mailbox = asiocore.cell_mailbox(hostnum,area.id,game_ip,game_port)
cell_mailbox记录了当前entity所在game进程的ip、port。当有对这个stub的RPC调用时,调用消息发到gate,然后gate根据cell_mailbox里的ip和port找到对应的game,把消息转发到那个game,那个gam再根据area.id找到哪个对象的area应该处理这个消息,最终交给该area对象调用对应的RPC函数。
使用base_mailbox和cell_mailbox都可以发起RPC,他们的区别在于base_mailbox支持entity迁移,而cell_mailbox不支持,所以一般stub只有cell_mailbox,avatar有base_mailbox和cell_mailbox。
关于更多弥赛亚引擎RPC实现细节,我在这里就不详细介绍了。
协议测试是协议内携带消息的测试。如果说协议是连接客户端与服务端的纽带,那协议测试就是对这一纽带可靠性验证的手段。上文有讲到,协议是双向的。游戏中既有客户端上传至服务端的上行协议,也有服务端回传给客户端的下行协议。
首先我们可以明确的一点是,服务端面对上行协议一般是无法监测其来源的,也就是说服务端在接收到任何匹配通讯规则的协议请求后,就会根据对应协议内容执行对应的逻辑。这样一来就会给一些别有用心的玩家一些操作的空间,他们会通过一些特殊手段让游戏客户端多次发送协议甚至尝试去破解并伪造协议,妄图欺骗服务器并破坏服务器的稳定。
所以在做协议测试的时候一定要谨记上行的协议是不可信的,需要完全抛开客户端因素来进行测试,只有服务器的代码足够健壮才能保证具有欺骗性的上行协议被拒之门外。
同样举个例子,假若小明没有留意自己手抖错点了100份外卖,如果店家不跟他来核对小明的确需要100份外卖就都送到家里门口的话,是不是就不太严谨呢。如果小明错点的是10000份外卖,那店家依然照做,是不是店家在短时间内就忙成一锅粥了呢?
由此可见,协议测试实际上是在测试服务器代码逻辑的严谨性与健壮性,需要服务器既能保证不被不合理的协议欺骗也要能保证处理非法协议时自身运行可以保持稳定。主要目的是防范玩家利用协议漏洞谋取不正当利益或者防范协议被利用来攻击服务器。其中涉及的重要程度我就不需要跟各位解释了吧。
要测试协议是否有漏洞,我们最关键的就是需要获取协议然后修改协议,达到欺骗服务端,从而验证服务端可靠性的目的。此类工具的原理一般都是围绕hook协议的方式展开。在我们组内目前可以提供了两种获取协议的方式以应对不同的测试场景。
在console中输入DEBUG指令可以在后台快速获取当前游戏操作下触发了哪些协议。比如施放了一次技能,可以从图片中看到,整个协议收发过程从客户端请求‘xxxSkillWrapper’开始到客户端请求‘xxxEndWrapper’结束。其中[C]标识的是上行协议,[S]标识的是下行协议,对应协议名称后面附带的是协议的参数。
这种方法的优点是实时方便快捷,直接能在console里面打印出希望获得的协议内容或协议参数,可以直接通过复制粘贴协议名称与参数达到重新发送协议的目的。但这种方法的缺点也非常明显,就是客户端日志或者非重点协议的打印一旦很多的话,就很容易把指令打印出来的协议给刷走。
我们组内也接入了工具组的协议测试工具,可以实现拦截协议,修改协议参数、多次发送协议等一系列操作,完全可以满足日常协议测试过程中所有的测试需求。
协议测试工具的运作流程是:
1. 启动游戏客户端,与协议测试工具建立连接。
2. 游戏客户端做相关操作触发相关协议。
3. 游戏客户端将该协议内容发往协议测试工具。
4. 协议测试工具收到游戏客户端发来的协议内容,修改协议内容后将篡改后的协议发往游戏客户端。
7. 游戏客户端收到篡改后的协议,并将其发往服务器。
前面铺垫了那么多,终于到了协议测试分享内容里最重要的环节:协议测试方法。整个协议测试阶段可以简单划分为客户端请求阶段和服务器收到请求后的处理阶段。一般情况下,很多人都会把关注点放在第二阶段但忽略了对客户端请求阶段的考虑。其实在我看来,完整的协议测试是需要同时考虑客户端发送协议和服务端处理协议的。下面会根据这两个方面具体展开来讲。
在使用协议测试工具捕捉协议的过程中,或许会遇到同一个协议客户端多次请求,亦或是不满足条件时,客户端依然很突兀的请求了一条协议等情况。在我看来协议测试过程中,倘若让自己遇到觉得协议发送时机有问题的情景,是有必要花时间去深究其中的逻辑原理的。
设定一个情景,比如小明打怪并掉落了一件装备,当它在客户端执行拾取操作的时候肯定是需要发送协议去通知服务器的。一般来说在处理拾取逻辑的时候,程序需要去兼容背包已满的情况,比如提示玩家背包已满拾取失败。那么在这种情景下,是不是可以直接在客户端上做背包已满的判断而不是放在服务端上去判定呢?
每一次的协议通讯最直观的体现就是产生了流量,在客户端请求阶段进行合理的判断就能在一定程度上减少因发送协议而带来的流量消耗,另一方面也能把一部分代码运行的压力从服务端施放出来,其收益是一目了然的。其实形式是多种多样的,比如输入框限制字符内容与长度、UI弹窗提示等。
协议测试过程中不少同学问我,在一些场景下感觉每次执行操作都会产生协议收发行为,自己往往会觉得可能存在刷协议影响服务器稳定性等隐患。但这种猜测一般都没有有力的支持证据,所以提建议给程序的话他们也大概率不会采纳。
我个人比较建议的是当出现这种情况的时候可以跟组内压力测试的同学沟通,把质疑的操作转化为一条服务器压力测试的用例,倘若真的存在性能问题,那我们必然也拥有了最有力的佐证。
相较于对客户端协议请求合理性的考虑,协议测试更重要且复杂的测试环节主要存在于对服务端协议处理逻辑的考量。因为客户端往往是不可信的,客户端判断逻辑不完善、异步时序操作甚至外挂行为都有可能导致服务端收到非法内容的协议数据,因而我们需要通过一定手段去测试服务器的处理逻辑是否强大,在处理不合理协议时能否表现稳定。
所以在设计用例的时候期望可以优先考虑客户端可以做到的异常情况,甚至是脱离客户端操作来考量对服务器响应的测试。因为服务器必须无视客户端,实现所有的关键点判断。
通常来说,每一条协议具体测试过程中都需要从这三个角度出发考虑:打破条件发送协议、伪造参数发送协议以及连续多次发送协议。
打破条件发送协议主要考察的是对于不满足条件情况下,发送携带正确参数的客户端协议时,服务器能否顺利拒绝不合理请求。此类问题可能潜藏于任何需要前置条件才能继续进行的游戏操作,比如日常跑环、商城、装备打造等诸多游戏系统。
此类问题通常是由服务端完全没有做校验或者服务器校验的判断条件考虑不全导致。在玩家不满足条件的情况下,可以通过直接发送下一阶段协议让服务器生成成功的返回。比如我身上并没有足够的货币却能通过协议购买商店的货物,那么外放出去一定是一个事故级别的隐患。
具体的测试方法一般是,在不满足条件的情况下发送客户端协议看是否能执行成功。在我们游戏中,很多情景下玩法的进行需要满足前置条件,比如:CD好了才可以放技能,等级到达上限后才能开启巅峰等级,拥有足够的钱才能创建社群等。这类协议我们需要关注这些前置条件,测试的关键就是构造各种不满足前置条件的情景并发送带有正确参数的协议,从而观察服务器是否可以拒绝来自客户端的不合理请求。
伪造参数发送协议主要关注的是服务器对于处理携带非法参数的协议时,是否能保证自身甚至数据库的稳定。需要考察此类问题的协议大部分为请求类和功能类的协议,比如请求某个玩家的详细信息、修改玩家名称、在聊天频道发送信息等等。外放事故最容易潜藏于当存在道具、游戏币等游戏内物品消耗情况下。
此类问题通常是服务端对数值合法性的校验不强导致的,没有判断请求中的数值是否处于合理的上下限范围内边界或者是对于处理异常值与特殊值的时候不够健壮。比方说,聊天频道发送信息的协议,如果没有对参数长度做限制,发送一个极大的字符串,就可能会引起服务器性能下架,接收者也会因为UI显示不了而导致崩溃。
具体的测试方法一般是,通过修改协议数值至策划设定范围之外来检验服务器对数值的校验是否强健。在具体数值用例的设计上,我们需要考虑三种情况:极值、特殊值与异常值。举个例子来说明,比如在测试活动开启时间,极值就是活动开启前后两个时间点,特殊值就是闰年的2月最后一天,异常值就是13月或32号。
连续多次发送协议主要考察服务器在接收到多条重复协议时,能否排除多余协议的干扰而给出正确的响应。此类问题经常潜伏于发奖、商城、交易和充值等与玩家利益相关的模块中,往往会被一些别有用心的玩家发现并恶意利用。由于此类模块与游戏经济系统以及玩家收益直接挂钩,所以一旦问题暴露就会大概率引发项目事故。
此类问题通常由两种情况导致:
1.服务端完全没有做校验,可以通过多次发送客户端协议就达成多次领奖等非正常操作;
2.尽管服务端做了校验,但发奖流程存在异步操作。比如当玩家领奖成功之后给玩家做一个标记,但领奖请求与做标记两个操作异步,就可能导致玩家多次请求奖励,在玩家被标记成功前收到的协议可能都会发奖。
具体的测试方法一般是,可以通过瞬时多次发送客户端请求协议来观察服务器反馈结果。在这一点上,利用协议测试工具能够很好的完成。 它有一个功能就是在短时间内连续发送指定数量的协议,可以很方便的达到连续发送的测试目的。
介绍完协议原理与协议测试方法后,最后我想补充一下关于协议测试工作的落地。其实在早期,我们的协议测试工作开展还是非常不完善的。一方面是对协议的理解不够深入,对其重要性的认识不是特别地深刻。另一方面也是经验不足,不知道协议测试专项如何在组内开展。在同其他项目组交流和学习过程中,我也收获了很多宝贵的经验与方法,在这里也一同分享给各位。
对组员协议测试的意识与能力的培养是很重要的。在推行协议测试过程中,我会很明显的发现有些组员会把协议测试作为日常功能验收的一个环节,而有些组员(多为新人同学)可能很久或是从未对负责功能做过协议测试。而且,新人同学在才接触协议测试的时候会有很多关于概念、工具与测试用例编写等方面的困难与疑问,在一定程度上是不太容易保证协议测试质量的。所以,我觉得需要重视对组内新人能力的培养,通过一些手段和方法帮助他们提高协议测试水平和质量。
首先能维护一篇系统的协议测试wiki以及一份通用的协议测试用例是非常好的,不仅可以帮助新人同学上手协议测试也可以帮助所有组员定期回顾协议测试的要点。如果条件允许的话甚至可以配合wiki内容与协议用例为新人准备协议测试入门的课程,对每期入组的新人进行系统的培训。在日常测试过程中,每个模块的协议测试用例可以由资历较深的带头组员来把关,从而在保证各个环节的协议测试质量的同时,也能保证组员的协议测试能力持续地成长。
之前,组内一直采用的方法就是在重要时间节点前对所有的模块的协议进行全量的回归,但这种方式的弊端其实是非常明显的。由于版本前期的时间比较宝贵,进行协议全量回归其实会短期增加大家的工作量,隐性地增加了版本的压力。而且在日常测试过程中,时不时有QA同学同我抱怨说,负责模块迭代过于频繁,但由于每次不知道会改到哪些协议,所以每次迭代完都要重新执行一次协议测试,使得测试压力非常大。
在协议测试方案不合理与组内协议测试效率低下的双重压力下,我选择了从其他优秀项目组学习交流经验。在同他们的沟通交流过程中我发现,协议监控工具可以很好地改观目前组内的尴尬局面。这个工具的设计理念就是帮助组员清楚哪些需求单对协议进行了改动。不仅能够达到提醒测试的目的,让组员更有针对性地进行协议测试。也可以将协议测试压力分散在日常测试工作中,让整个测试工作的节奏更加合理。
协议本身也会涉及重要与否,考虑的出发点可以围绕影响玩家数据与影响核心体验两个方面展开,通常涉及主流程玩法、奖励发放,数值养成等模块。所以在日常测试过程中,我们也会配合协议监控工具去标识其中比较重要的协议,方便执行定期review或集中回归等更加稳固的协议保障手段。
此外,我们还增加了协议监控未确认提醒、易协作协议测试提醒等其他小功能来进一步提高协议监控工具的使用效率。
协议测试看似简单,但实则也有很多值得思考与深挖的内容。通过这次较为系统的总结与归纳,我仍然学到了很多新的内容。感谢内部学习平台各位大佬提供的知识总结让我真正明白了协议实现方法等较为底层的知识,同时也感谢68的大神与小树二位大佬无私地同我分享了很多协议测试方式与方法。希望我的分享能给各位带来新的启发。
评论 (1)