【原創(chuàng)博文恤溶,轉(zhuǎn)載請(qǐng)注明出處!】
游戲中的消息多使用長(zhǎng)連接機(jī)制帜羊,以確保多個(gè)玩家之間消息和動(dòng)作的同步咒程。在使用的過(guò)程中,我們經(jīng)常擔(dān)心由于網(wǎng)絡(luò)或其他原因?qū)е孪⑦z漏或順序錯(cuò)亂讼育。下面就針對(duì)這兩點(diǎn)談?wù)勎业奶幚矸桨浮?/p>
方案大綱
* 1.定時(shí)器輪詢服務(wù)器端最后消息seqNum帐姻,確保本地消息沒(méi)有遺漏稠集。如果seqNum與服務(wù)端不一致,則根據(jù)本地已執(zhí)行的最大seqNum饥瓷,查詢此seqNum后面的消息mesArr剥纷。
* 2.對(duì)獲得的消息數(shù)組mesArr按照seqNum排序,待執(zhí)行呢铆】昶瑁【注:seqNum是每條消息的順序,服務(wù)器下發(fā)消息的時(shí)候遞增seqNum刺洒,客戶端對(duì)收到的消息根據(jù)seqNum排序鳖宾,依次執(zhí)行,可以保證消息執(zhí)行秩序正確逆航、不遺漏】鼎文。
* 3.驗(yàn)證自開(kāi)始查詢服務(wù)器消息 到 獲取了服務(wù)器消息并將消息已排好序待執(zhí)行的過(guò)程中,起初被查詢的消息否已經(jīng)收到并執(zhí)行了因俐, status:true(已執(zhí)行,跳到步驟4拇惋,到此結(jié)束); status:false(沒(méi)執(zhí)行抹剩,則跳到步驟5)撑帖。
* 4.對(duì)3中的驗(yàn)證結(jié)果status = true,說(shuō)明在獲取服務(wù)器端消息的過(guò)程中,被查詢的消息已經(jīng)通過(guò)長(zhǎng)連接獲取到并執(zhí)行了澳眷,應(yīng)該廢棄查詢到的結(jié)果 并重置定時(shí)器胡嘿。
* 5.對(duì)3中的驗(yàn)證結(jié)果status = false,說(shuō)明有遺漏的消息。開(kāi)始處理遺漏的消息钳踊,這個(gè)過(guò)程中衷敌,允許長(zhǎng)連接接收消息到隊(duì)列中,但是不能執(zhí)行消息(防止那遺漏的消息突然推送過(guò)來(lái)被執(zhí)行拓瞪,然后主動(dòng)從服務(wù)器獲取的這條消息接下來(lái)又被執(zhí)行一次??)缴罗。
* 6.設(shè)置shouldExecuteImmediatelyLock = false;
* 7.檢查是否到達(dá)最大錯(cuò)誤次數(shù)。是:恢復(fù)牌局(就此結(jié)束); 否:處理遺漏的數(shù)據(jù)(跳到第8步);
* 8.按順序執(zhí)行完從服務(wù)器獲取的消息祭埂。
* 9.遞歸一下本地消息隊(duì)列messageQueue面氓,看看有沒(méi)有接下來(lái)需要執(zhí)行的消息。
* 10.處理完消息隊(duì)列中的消息后蛆橡,再設(shè)置shouldExecuteImmediatelyLock -> true舌界,開(kāi)啟執(zhí)行長(zhǎng)連接消息權(quán)限。
下面簡(jiǎn)單解釋說(shuō)明一下每步驟:
Step one :
定時(shí)輪詢并不是輪詢服務(wù)器消息到本地去執(zhí)行航罗,(消息主要靠長(zhǎng)連接推送)禀横,這個(gè)輪詢是檢查本地消息是否與服務(wù)端下發(fā)的消息同步了。即使客戶端消息遺漏了粥血,也便于我們?nèi)プ粉欉z漏的消息柏锄。
//數(shù)據(jù)初始化成功,可以處理隊(duì)列中的消息了
cc.vv.dispatcher.on(cc.vv.eventName.cardInit,function(event){
//開(kāi)啟消息查詢定時(shí)器 30s
this.timer = setInterval(function(){
//查看服務(wù)端seqNum
this.boardMsgSeqQuery();
}.bind(this),30000);
if (this.messageQueue.length) {
//暫時(shí)不下發(fā)正在推送的消息
this.shouldExecuteImmediatelyLock = false;
//將數(shù)據(jù)初始化之前消息隊(duì)列中暫存的消息全部處理完畢
this.dealWithFormalMsg();
}
}.bind(this));
上面就是我的輪詢酿箭。需要指出的是,我在項(xiàng)目里面增加了一個(gè)定時(shí)器復(fù)位的功能趾娃,主要是避免反復(fù)查詢缭嫡,這個(gè)后面詳細(xì)解釋。
cc.vv.dispatcher.on(cc.vv.eventName.resetSeqNumQueryTimer,function(event){
console.log("復(fù)位seqnum查詢定時(shí)器");
clearInterval(this.timer);
this.timer = setInterval(function(){
this.boardMsgSeqQuery();
}.bind(this),30000);
}.bind(this));
Step 2 :
對(duì)獲得的消息msgArr數(shù)組按照seqnum排序抬闷,待執(zhí)行妇蛀。因?yàn)檫@些請(qǐng)求之前本地沒(méi)有收到的消息,存在于服務(wù)器端笤成,現(xiàn)在從服務(wù)端通過(guò)http的response返回的评架,所以不存在丟失的問(wèn)題,直接將它們按seqNum排好序炕泳,準(zhǔn)備執(zhí)行纵诞。
// 對(duì)獲得的消息msgArr數(shù)組按照seqnum排序,待執(zhí)行培遵。
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 :
驗(yàn)證自開(kāi)始查詢服務(wù)器消息 到 獲取了服務(wù)器消息并將消息已排好序待執(zhí)行的過(guò)程中浙芙,起初被查詢的消息否已經(jīng)收到并執(zhí)行了, status:true(已執(zhí)行,跳到步驟4籽腕,到此結(jié)束)嗡呼; status:false(沒(méi)執(zhí)行,則跳到步驟5)
if (this.currentQuerySeqNum < cc.vv.globalVariables.seqNum) {
// 4.對(duì)3中的驗(yàn)證結(jié)果status = true,說(shuō)明在獲取服務(wù)器端消息的過(guò)程中皇耗,被查詢的消息已經(jīng)通過(guò)長(zhǎng)連接獲取到并執(zhí)行了南窗,應(yīng)該廢棄查詢到的結(jié)果 并重置定時(shí)器
cc.vv.dispatcher.emit(cc.vv.eventName.resetSeqNumQueryTimer);
}
這個(gè)判斷很簡(jiǎn)單,因?yàn)槲以诓樵冎皩⒖蛻舳艘呀?jīng)執(zhí)行的最后一條消息seqNum記錄在this.currentQuerySeqNum變量中廊宪,由于查詢的過(guò)程中推送的消息仍舊在處理矾瘾,并且每處理一條消息都會(huì)記錄該消息的seqNum到 cc.vv.globalVariables.seqNum,所以比較this.currentQuerySeqNum與 cc.vv.globalVariables.seqNum即可箭启。(補(bǔ)充一點(diǎn):在查詢過(guò)程中收到了推送的消息說(shuō)明長(zhǎng)連接沒(méi)啥問(wèn)題,也可以將http消息查詢定時(shí)器重置一下蛉迹,避免過(guò)多的流量傅寡。)
Step 4 :
對(duì)3中的驗(yàn)證結(jié)果status = true,說(shuō)明在獲取服務(wù)器端消息的過(guò)程中,被查詢的消息已經(jīng)通過(guò)長(zhǎng)連接獲取到并執(zhí)行了北救,應(yīng)該廢棄查詢到的結(jié)果 并重置定時(shí)器
Step 5 :
對(duì)3中的驗(yàn)證結(jié)果status = false,說(shuō)明有遺漏的消息荐操。開(kāi)始處理遺漏的消息,這個(gè)過(guò)程中珍策,允許長(zhǎng)連接接收消息到隊(duì)列中托启,但是不能執(zhí)行消息(防止那遺漏的消息突然推送過(guò)來(lái)被執(zhí)行,然后主動(dòng)從服務(wù)器獲取的這條消息接下來(lái)又被執(zhí)行一次??)
Step 6 :
設(shè)置shouldExecuteImmediatelyLock = false
Step 7 :
檢查是否到達(dá)最大錯(cuò)誤次數(shù)攘宙。是:恢復(fù)牌局(就此結(jié)束); 否:處理遺漏的數(shù)據(jù)(跳到第8步)
// 6.設(shè)置shouldExecuteImmediatelyLock = false;
this.shouldExecuteImmediatelyLock = false; //允許長(zhǎng)連接接收消息到隊(duì)列中屯耸,但是不能執(zhí)行消息
this.errorCount += 1;
if (this.errorCount == thresholdErrorCount) { //達(dá)到最大錯(cuò)誤數(shù) 恢復(fù)牌局
cc.vv.dispatcher.emit(cc.vv.eventName.recoverBoard);
console.log("因遺漏消息次數(shù)太多導(dǎo)致恢復(fù)牌局一次");
}else{
// 8.按順序執(zhí)行完從服務(wù)器獲取的消息拐迁。
this.executeEachMissedMessage(tempMsgQueue);
}
設(shè)置好恢復(fù)牌局閾值 (說(shuō)得low??一點(diǎn)就是“臨界值”?(? ???ω??? ?)?),如下:
const thresholdErrorCount = 8;
如果由于網(wǎng)絡(luò)或物理環(huán)境極其差的原因?qū)е峦扑瓦^(guò)程中的消息屢次有遺漏疗绣,不斷通過(guò)http向服務(wù)器查詢遺漏的消息并執(zhí)行也是沒(méi)啥問(wèn)題的线召。但是安全起見(jiàn),累計(jì)錯(cuò)誤次數(shù)過(guò)多還是恢復(fù)一下游戲場(chǎng)景比較好多矮,這樣相當(dāng)于所有的數(shù)據(jù)與服務(wù)器絕對(duì)一致(畢竟推送過(guò)程中很多數(shù)據(jù)都是在本地記錄的缓淹,誰(shuí)也不敢保證在推送不及時(shí)的情況下,能夠從http主動(dòng)獲取的消息與推送的消息中完美滴解析出需要的數(shù)據(jù)并融合在本地的數(shù)據(jù)中??)塔逃。
Step 8 :
按順序執(zhí)行完從服務(wù)器獲取的消息
Step 9 :
遞歸一下本地消息隊(duì)列messageQueue讯壶,看看有沒(méi)有接下來(lái)需要執(zhí)行的消息
Step 10 :
處理完消息隊(duì)列中的消息后,再設(shè)置shouldExecuteImmediatelyLock -> true湾盗,開(kāi)啟執(zhí)行長(zhǎng)連接消息權(quán)限
executeEachMissedMessage(tempMsgQueue){
//開(kāi)始按順序處理這些消息了
for (let index = 0; index < tempMsgQueue.length; index++) {
const message = tempMsgQueue[index];
//seqNum標(biāo)志++
cc.vv.globalVariables.seqNum = cc.vv.globalVariables.seqNum + 1;
let msgId = String(JSON.parse(message.msgId));
this.setMaxMsgId(msgId);
//執(zhí)行當(dāng)前這條消息
cc.vv.cardDataMgr.pushInfoHandle(message);
}
//9.遞歸一下本地消息隊(duì)列messageQueue鹏溯,看看有沒(méi)有接下來(lái)需要執(zhí)行的消息。
this.recursiveMessageBodyThroughMessageQueue();
// 10.處理完消息隊(duì)列中的消息后淹仑,再設(shè)置shouldExecuteImmediatelyLock -> true丙挽,開(kāi)啟執(zhí)行長(zhǎng)連接消息權(quán)限。
this.shouldExecuteImmediatelyLock = true;
},
由于http方式能夠從服務(wù)器獲取到所有遺漏的消息匀借,所以將這些消息全部按seqNum排序颜阐,然后一股腦地one by one執(zhí)行掉就可以了(這過(guò)程中不用考慮下條消息的seqNum是否比上條消息seqNum大1,因?yàn)榉?wù)端消息就這樣給你了吓肋,你還想哪樣凳怨?有問(wèn)題也是后臺(tái)背鍋吧??)。前面說(shuō)句執(zhí)行這些http獲取的消息過(guò)程中是鬼,長(zhǎng)連接推送過(guò)來(lái)的消息只會(huì)存在消息隊(duì)列中肤舞,并不允許執(zhí)行,那么執(zhí)行完http拿到的遺漏消息均蜜,我們還需要看看本地消息隊(duì)列中有沒(méi)有接下來(lái)需要執(zhí)行的消息了李剖。接下來(lái)的消息怎么確定呢??,還是通過(guò)下面方式判斷囤耳。
//待執(zhí)行的消息num = 本地已經(jīng)執(zhí)行過(guò)得消息num + 1
let prepareToExcuteMsgSeqNum = cc.vv.globalVariables.seqNum + 1;
下面看看遞歸本地消息隊(duì)列的方法實(shí)現(xiàn)吧??
/**
* 從消息隊(duì)列中遞歸處理待執(zhí)行消息
*/
recursiveMessageBodyThroughMessageQueue(){
//上條消息執(zhí)行完之后篙顺,就去 隊(duì)列messageQueue( ? arr.length > 0)里面取待執(zhí)行的下一條,如果取不到,繼續(xù)等待充择。德玫。。
if (!this.messageQueue.length) return;
//待執(zhí)行的消息num = 本地已經(jīng)執(zhí)行過(guò)得消息num + 1
let prepareToExcuteMsgSeqNum = cc.vv.globalVariables.seqNum + 1;
//遍歷 this.messageQueue椎麦,查找 prepareToExcuteMsgSeqNum序號(hào)的消息
for (let index = 0; index < this.messageQueue.length; index++) {
const messageObject = this.messageQueue[index];
let content = JSON.parse(messageObject.content);
if (content.seqNum == prepareToExcuteMsgSeqNum) {
//存儲(chǔ)已執(zhí)行動(dòng)作消息的最大“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;
//則去執(zhí)行當(dāng)前這條消息
cc.vv.cardDataMgr.pushInfoHandle(this.messageQueue[index]);
//遞歸一下
this.recursiveMessageBodyThroughMessageQueue();
}
}
},
嗯宰僧,處理完http從服務(wù)器獲取的遺漏消息和本地消息隊(duì)列中的消息,我們?cè)僭O(shè)置消息執(zhí)行權(quán)限 this.shouldExecuteImmediatelyLock = true;也就是允許處理長(zhǎng)連接推送消息功能观挎。
好了琴儿,本次的分享到此為止段化,如果你發(fā)現(xiàn)上述處理方案有缺陷或需要改進(jìn),歡迎留言凤类。當(dāng)然穗泵,如果你有更好的解決方案,歡迎賜教谜疤,吾當(dāng)不慎感激??佃延。