谈谈游戏中长连接消息防丢失的处理方案

【原创博文,转载请注明出处!】
游戏中的消息多使用长连接机制,以确保多个玩家之间消息和动作的同步。在使用的过程中,我们经常担心由于网络或其他原因导致消息遗漏或顺序错乱。下面就针对这两点谈谈我的处理方案。

方案大纲

* 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;也就是允许处理长连接推送消息功能。

好了,本次的分享到此为止,如果你发现上述处理方案有缺陷或需要改进,欢迎留言。当然,如果你有更好的解决方案,欢迎赐教,吾当不慎感激😊。

praise.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容