【原创博文,转载请注明出处!】
游戏中的消息多使用长连接机制,以确保多个玩家之间消息和动作的同步。在使用的过程中,我们经常担心由于网络或其他原因导致消息遗漏或顺序错乱。下面就针对这两点谈谈我的处理方案。
方案大纲
* 1.定时器轮询服务器端最后消息seqNum,确保本地消息没有遗漏。如果seqNum与服务端不一致,则根据本地已执行的最大seqNum,查询此seqNum后面的消息mesArr。
* 2.对获得的消息数组mesArr按照seqNum排序,待执行。【注:seqNum是每条消息的顺序,服务器下发消息的时候递增seqNum,客户端对收到的消息根据seqNum排序,依次执行,可以保证消息执行秩序正确、不遗漏】。
* 3.验证自开始查询服务器消息 到 获取了服务器消息并将消息已排好序待执行的过程中,起初被查询的消息否已经收到并执行了, status:true(已执行,跳到步骤4,到此结束); status:false(没执行,则跳到步骤5)。
* 4.对3中的验证结果status = true,说明在获取服务器端消息的过程中,被查询的消息已经通过长连接获取到并执行了,应该废弃查询到的结果 并重置定时器。
* 5.对3中的验证结果status = false,说明有遗漏的消息。开始处理遗漏的消息,这个过程中,允许长连接接收消息到队列中,但是不能执行消息(防止那遗漏的消息突然推送过来被执行,然后主动从服务器获取的这条消息接下来又被执行一次☹️)。
* 6.设置shouldExecuteImmediatelyLock = false;
* 7.检查是否到达最大错误次数。是:恢复牌局(就此结束); 否:处理遗漏的数据(跳到第8步);
* 8.按顺序执行完从服务器获取的消息。
* 9.递归一下本地消息队列messageQueue,看看有没有接下来需要执行的消息。
* 10.处理完消息队列中的消息后,再设置shouldExecuteImmediatelyLock -> true,开启执行长连接消息权限。
下面简单解释说明一下每步骤:
Step one :
定时轮询并不是轮询服务器消息到本地去执行,(消息主要靠长连接推送),这个轮询是检查本地消息是否与服务端下发的消息同步了。即使客户端消息遗漏了,也便于我们去追踪遗漏的消息。
//数据初始化成功,可以处理队列中的消息了
cc.vv.dispatcher.on(cc.vv.eventName.cardInit,function(event){
//开启消息查询定时器 30s
this.timer = setInterval(function(){
//查看服务端seqNum
this.boardMsgSeqQuery();
}.bind(this),30000);
if (this.messageQueue.length) {
//暂时不下发正在推送的消息
this.shouldExecuteImmediatelyLock = false;
//将数据初始化之前消息队列中暂存的消息全部处理完毕
this.dealWithFormalMsg();
}
}.bind(this));
上面就是我的轮询。需要指出的是,我在项目里面增加了一个定时器复位的功能,主要是避免反复查询,这个后面详细解释。
cc.vv.dispatcher.on(cc.vv.eventName.resetSeqNumQueryTimer,function(event){
console.log("复位seqnum查询定时器");
clearInterval(this.timer);
this.timer = setInterval(function(){
this.boardMsgSeqQuery();
}.bind(this),30000);
}.bind(this));
Step 2 :
对获得的消息msgArr数组按照seqnum排序,待执行。因为这些请求之前本地没有收到的消息,存在于服务器端,现在从服务端通过http的response返回的,所以不存在丢失的问题,直接将它们按seqNum排好序,准备执行。
// 对获得的消息msgArr数组按照seqnum排序,待执行。
tempMsgQueue.sort(function(object1,object2){
return JSON.parse(object1.content).seqNum - JSON.parse(object2.content).seqNum;
// return object1.content.seqNum - object2.content.seqNum;
});
Step 3 :
验证自开始查询服务器消息 到 获取了服务器消息并将消息已排好序待执行的过程中,起初被查询的消息否已经收到并执行了, status:true(已执行,跳到步骤4,到此结束); status:false(没执行,则跳到步骤5)
if (this.currentQuerySeqNum < cc.vv.globalVariables.seqNum) {
// 4.对3中的验证结果status = true,说明在获取服务器端消息的过程中,被查询的消息已经通过长连接获取到并执行了,应该废弃查询到的结果 并重置定时器
cc.vv.dispatcher.emit(cc.vv.eventName.resetSeqNumQueryTimer);
}
这个判断很简单,因为我在查询之前将客户端已经执行的最后一条消息seqNum记录在this.currentQuerySeqNum变量中,由于查询的过程中推送的消息仍旧在处理,并且每处理一条消息都会记录该消息的seqNum到 cc.vv.globalVariables.seqNum,所以比较this.currentQuerySeqNum与 cc.vv.globalVariables.seqNum即可。(补充一点:在查询过程中收到了推送的消息说明长连接没啥问题,也可以将http消息查询定时器重置一下,避免过多的流量。)
Step 4 :
对3中的验证结果status = true,说明在获取服务器端消息的过程中,被查询的消息已经通过长连接获取到并执行了,应该废弃查询到的结果 并重置定时器
Step 5 :
对3中的验证结果status = false,说明有遗漏的消息。开始处理遗漏的消息,这个过程中,允许长连接接收消息到队列中,但是不能执行消息(防止那遗漏的消息突然推送过来被执行,然后主动从服务器获取的这条消息接下来又被执行一次☹️)
Step 6 :
设置shouldExecuteImmediatelyLock = false
Step 7 :
检查是否到达最大错误次数。是:恢复牌局(就此结束); 否:处理遗漏的数据(跳到第8步)
// 6.设置shouldExecuteImmediatelyLock = false;
this.shouldExecuteImmediatelyLock = false; //允许长连接接收消息到队列中,但是不能执行消息
this.errorCount += 1;
if (this.errorCount == thresholdErrorCount) { //达到最大错误数 恢复牌局
cc.vv.dispatcher.emit(cc.vv.eventName.recoverBoard);
console.log("因遗漏消息次数太多导致恢复牌局一次");
}else{
// 8.按顺序执行完从服务器获取的消息。
this.executeEachMissedMessage(tempMsgQueue);
}
设置好恢复牌局阈值 (说得low👎一点就是“临界值”⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄),如下:
const thresholdErrorCount = 8;
如果由于网络或物理环境极其差的原因导致推送过程中的消息屡次有遗漏,不断通过http向服务器查询遗漏的消息并执行也是没啥问题的。但是安全起见,累计错误次数过多还是恢复一下游戏场景比较好,这样相当于所有的数据与服务器绝对一致(毕竟推送过程中很多数据都是在本地记录的,谁也不敢保证在推送不及时的情况下,能够从http主动获取的消息与推送的消息中完美滴解析出需要的数据并融合在本地的数据中😊)。
Step 8 :
按顺序执行完从服务器获取的消息
Step 9 :
递归一下本地消息队列messageQueue,看看有没有接下来需要执行的消息
Step 10 :
处理完消息队列中的消息后,再设置shouldExecuteImmediatelyLock -> true,开启执行长连接消息权限
executeEachMissedMessage(tempMsgQueue){
//开始按顺序处理这些消息了
for (let index = 0; index < tempMsgQueue.length; index++) {
const message = tempMsgQueue[index];
//seqNum标志++
cc.vv.globalVariables.seqNum = cc.vv.globalVariables.seqNum + 1;
let msgId = String(JSON.parse(message.msgId));
this.setMaxMsgId(msgId);
//执行当前这条消息
cc.vv.cardDataMgr.pushInfoHandle(message);
}
//9.递归一下本地消息队列messageQueue,看看有没有接下来需要执行的消息。
this.recursiveMessageBodyThroughMessageQueue();
// 10.处理完消息队列中的消息后,再设置shouldExecuteImmediatelyLock -> true,开启执行长连接消息权限。
this.shouldExecuteImmediatelyLock = true;
},
由于http方式能够从服务器获取到所有遗漏的消息,所以将这些消息全部按seqNum排序,然后一股脑地one by one执行掉就可以了(这过程中不用考虑下条消息的seqNum是否比上条消息seqNum大1,因为服务端消息就这样给你了,你还想哪样?有问题也是后台背锅吧😁)。前面说句执行这些http获取的消息过程中,长连接推送过来的消息只会存在消息队列中,并不允许执行,那么执行完http拿到的遗漏消息,我们还需要看看本地消息队列中有没有接下来需要执行的消息了。接下来的消息怎么确定呢😶,还是通过下面方式判断。
//待执行的消息num = 本地已经执行过得消息num + 1
let prepareToExcuteMsgSeqNum = cc.vv.globalVariables.seqNum + 1;
下面看看递归本地消息队列的方法实现吧😕
/**
* 从消息队列中递归处理待执行消息
*/
recursiveMessageBodyThroughMessageQueue(){
//上条消息执行完之后,就去 队列messageQueue( ? arr.length > 0)里面取待执行的下一条,如果取不到,继续等待。。。
if (!this.messageQueue.length) return;
//待执行的消息num = 本地已经执行过得消息num + 1
let prepareToExcuteMsgSeqNum = cc.vv.globalVariables.seqNum + 1;
//遍历 this.messageQueue,查找 prepareToExcuteMsgSeqNum序号的消息
for (let index = 0; index < this.messageQueue.length; index++) {
const messageObject = this.messageQueue[index];
let content = JSON.parse(messageObject.content);
if (content.seqNum == prepareToExcuteMsgSeqNum) {
//存储已执行动作消息的最大“msgId”
// let msgId = JSON.parse(messageObject.msgId);
let msgId = String(JSON.parse(messageObject.msgId));
this.setMaxMsgId(msgId);
//本地消息 seqNum++
cc.vv.globalVariables.seqNum = cc.vv.globalVariables.seqNum + 1;
//则去执行当前这条消息
cc.vv.cardDataMgr.pushInfoHandle(this.messageQueue[index]);
//递归一下
this.recursiveMessageBodyThroughMessageQueue();
}
}
},
嗯,处理完http从服务器获取的遗漏消息和本地消息队列中的消息,我们再设置消息执行权限 this.shouldExecuteImmediatelyLock = true;也就是允许处理长连接推送消息功能。
好了,本次的分享到此为止,如果你发现上述处理方案有缺陷或需要改进,欢迎留言。当然,如果你有更好的解决方案,欢迎赐教,吾当不慎感激😊。