忏悔篇~谷歌protobuf的defaullt和ulua解析巨坑

避免bug之路真是路漫漫其修远兮啊!其实发现这个问题,还是得益于我们良好的服务端架构。我先介绍我们公司的架构。

1.项目结构

中间语言运用谷歌的protobuf,客户端为了热更新引入ulua包,大部分业务逻辑采用lua完成。服务端用基于.Net Core3.0的C#(保证服务端可以跨平台)。前后端协议利用proto定义的枚举,以及 proto定义的message的结构。例如登录协议。客户端new一个含有 登录message结构的 lua表。基于tcp/ip协议栈。发送到服务端。Server同样解析网络包。发现是5000的协议。服务端的结构是每一条Action对应一个C#文件。命名规范是Action5000.cs.拿到5000后,在前面加上Action,通过反射找到Action5000这个文件(这个描述不太准确)。然后通过包中userID 实例化到内存中这个用户的Model信息(实例化的过程就是通过UserID作为主键 查询redis or mySQL加载这个用户数据到内存中,当然包括加线程读写锁)。然后处理业务逻辑,修改Model数据,回包,Model回写reids,释放用户Model。客户端拿到回包,做相应的前端逻辑。

2.Model

所谓Model就是用户的个人信息嘛,根据业务逻辑肯定要分为不同的类型的Model。例如个人信息(名字,性别,等),装备(装备栏信息,武器品质等等),这个肯定不能放在同一个表中,不然去查询修改都会有很大的负载压力。这样根据业务逻辑分Model,即MySQL的数据分表,天然达到负载均衡的需求。

syntax = "proto2"; //注意是2.0版本
message Play
{
 required int32 UserID       = 1; //主键
 required int32 Version       = 2;//proto版本号
 required string NickName    = 3;//玩家名称
 required int32 Sex          = 4;//性别
 required int32  ChangenNameTimes = 5[Default = 0];//修改名字的次数
 required int32  BattleTimes = 6;//战斗次数
 
 

}

3.网络包结构

我们的与业务服务器通信的网络包结构大致如下(拿5000为例)
对于客户端而言发送包
Header + [ RequestMessage5000 + ClientProtoVersion ]
对于客户端而言回馈包
Header + [ ErrorCode + ResponseMessage5000 + ExpandPack ]

ClientProtoVersion Model Proto客户端版本号
Herder不用具体介绍了,包含包体大小等,处理一些收包粘包等问题。
ErrorCode 是回包状态,例如10000时,证明该包超时未发送成功,或者未接收到 客户端超时自己调用回包,这个时候可能需要提示用户网络不佳等信息。等于0时,就是正常回包。
ResponseMessage5000 业务处理需要的回包。
ExpandPack 扩展包, 这个扩展包是解决信息同步问题。例如客户端修改了名字 NickName ,同时 ChangenNameTimes ++,然后Server就会把这两个字段放在回馈包中,你可能会想 ,既然Model是分开的 那么又改了名字的Model,又改了武器的Model怎么办。这个是通过事先定义好的枚举区别开的。客户端解析这个扩展包时也会根据枚举分别修改客户端本地的不同Model信息。而完整包和差异包也是根据这个而言的。意思是只放入修改的字段还是整个Model的所有结构。

4.完整包和差异包

差异包为了减少通信带宽,等处理上表现都十分优化。那它是怎么实现的?例如登录时客户端没有任何用户信息,这个时候不用想就是完整包,所以登录是回包会十分巨大。所以客户端要做登录等待UI表现。进度条等。个人猜想大部分游戏都是如此吧。而后面的包,每次Action都可能只改变很少的数据所有没必要整个Model的结构都放入扩展包中。

5.如何判断返回完整包和差异包

大家可以仔细观察上面的Model结构,有一个Version字段,其实所有的Model都有 UserID 和Version这两个字段。 UserID是主键,必须要有,Version就是为了判断这个包的。具体流程如下。
首先客户端登录 5000,客户端发送客户端版本号,第一次没有信息自然都是0了,发送到服务端之后,服务端会把所有的Model放入扩展包中,同步给客户端。这个时候客户端的版本号和服务端的版本号是一致的。
第二步 用户修改战斗次数 协议5003 ,同样会发送客户端所有Model的版本号
假如说发送的 Play 的版本号 是 9。上面讲等包开始处理时,先实例化用户信息,拿到所有不同类型的Model实例,Catch所有的Model版本号(注意这个是开始处理包之前的Model的版本号 自然Play 的版本号 也是 9),然后经过5003,修改数据,服务端修改数据时会调用不同的Model的对应BeHavior实例的ModelLock方法,目的是提高版本号,即修改过后此时Play的Version为 10。在回包时,遍历所有的Model,对比修改过后的Model版本号和客户端上传的版本号,证明该Model有变化,反之跳过,再对比前面Catch的版本号,和客户端版本号,是否一致,一致证明该变化是由于此Action影响的,就标志返回差异包,反之就是其他原因造成了变化,就返回完整包。

6.客户端解析扩展包

服务端返回5003包之后,拿到扩展包,解析成不同Model 的lua后是一个表结构,即Play表,里面只有 Version 和 BattleTimes 。没有其他多余的结构。修改客户端的Model信息。看到这里觉得没什么问题,思路清晰。
但是问题出在Play表中不只有 Version 和 Sex ,用过lua的小伙伴都知道,lua有元表[–index 可以指向一个表,访问的时候,先看表中是否有需要的对象,没有去–index指向的表中查找]。通过Proto结构转为lua表时,回包有的字段都塞在表中,没有的字段,如果在proto中附有初始值,会放在Play的元表中,防止外部获取,为空的情况,拿到lua表后 依次遍历表里结构是否存在, NickName:这个字段没有存在,就去元表中找,也没有初始值,就不会修改本地Model中的NickName, BattleTimes :表中存在,就去修改本地Model,ChangenNameTimes 表中不存在,访问元表,元表中存在初始值为0,,修改本地Model的数据为0.BUG就在这里。这肯定不是我们想要的。

7.BUG表现

本人负责一个新手引导的功能,里面有一个环节的功能是,先起名字,然后引导去打战斗一次。
起名字后,由于其他原因,回包是完整包,客户端的 ChangenNameTimes 就是 1。然后去打战斗,战斗结束,修改 BattleTimes ,此时回包为差异包,但是由于 ChangenNameTimes 在proto中定义有初始值,导致本地Model的ChangenNameTimes 值又被修改为0,造成战斗完又起名字。

8.BUG解决和感想

syntax = "proto2"; //注意是2.0版本
message Play
{
 required int32 UserID       = 1; //主键
 required int32 Version       = 2;//proto版本号
 required string NickName    = 3;//玩家名称
 required int32 Sex          = 4;//性别
 required int32  ChangenNameTimes = 5;//修改名字的次数
 required int32  BattleTimes = 6;//战斗次数
}

历史原因前人加的默认值 修改过后的Proto结构,去掉了默认值。
修改这个BUG花费了快4个小时,讲到这里老泪纵横,加油!热爱技术,追求技术。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值