談?wù)動(dòng)螒蛑虚L(zhǎng)連接消息防丟失的處理方案

【原創(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)不慎感激??佃延。

praise.jpeg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市夷磕,隨后出現(xiàn)的幾起案子履肃,更是在濱河造成了極大的恐慌,老刑警劉巖坐桩,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尺棋,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡绵跷,警方通過(guò)查閱死者的電腦和手機(jī)膘螟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碾局,“玉大人荆残,你說(shuō)我怎么就攤上這事【坏保” “怎么了内斯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)像啼。 經(jīng)常有香客問(wèn)我俘闯,道長(zhǎng),這世上最難降的妖魔是什么忽冻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任真朗,我火速辦了婚禮,結(jié)果婚禮上甚颂,老公的妹妹穿的比我還像新娘蜜猾。我一直安慰自己,他們只是感情好振诬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著衍菱,像睡著了一般赶么。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脊串,一...
    開(kāi)封第一講書(shū)人閱讀 51,198評(píng)論 1 299
  • 那天辫呻,我揣著相機(jī)與錄音清钥,去河邊找鬼。 笑死放闺,一個(gè)胖子當(dāng)著我的面吹牛祟昭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怖侦,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼篡悟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了匾寝?” 一聲冷哼從身側(cè)響起搬葬,我...
    開(kāi)封第一講書(shū)人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎艳悔,沒(méi)想到半個(gè)月后急凰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡猜年,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年抡锈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乔外。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡床三,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出袁稽,到底是詐尸還是另有隱情勿璃,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布推汽,位于F島的核電站补疑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏歹撒。R本人自食惡果不足惜莲组,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暖夭。 院中可真熱鬧锹杈,春花似錦、人聲如沸迈着。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)裕菠。三九已至咬清,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旧烧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工影钉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掘剪。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓平委,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親夺谁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子廉赔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 導(dǎo)出 導(dǎo)入
    村南一枝花閱讀 168評(píng)論 0 0
  • 今天去百榮,走渴了予权,特意去找以前愛(ài)喝的綠豆湯昂勉,沒(méi)想到那家還在,喝了一碗扫腺,瞬間感覺(jué)很滿足岗照。核心:找感覺(jué),只要你能給對(duì)...
    王佳歡雪閱讀 148評(píng)論 0 0
  • 上大學(xué)的時(shí)候,總感覺(jué)時(shí)間過(guò)的很慢躁劣,慢到一天可以呆在圖書(shū)館看一本完整的書(shū)籍迫吐,殊不知,那時(shí)候才是最幸福的账忘。我并不是說(shuō)自...
    迷糊小陳閱讀 838評(píng)論 0 0
  • 徘徊 原來(lái)不是江南 煙雨蒙蒙 暖風(fēng)喃喃 只是 像極了江南 南城古鎮(zhèn) 亭臺(tái)樓榭 小橋流水 烏篷船舶 還有盈盈潤(rùn)潤(rùn)的月...
    書(shū)舒__閱讀 201評(píng)論 3 3