游戏服务器框架升级-无入侵式代理解决方案

框架升级的目的

主要为了提高服务器的整体承载能力以及游戏的体验。

框架升级遇到的主要障碍

主要遇到的困难,早期最原始的方案,就是直接进行业务代码切割,但对于一个线上

运营了多年的项目,系统模块耦合得非常紧密,这样的改动工作量大到无法评估,该方案可

执行性非常差,即使能够执行落地,开发工作时间长,由于涉及改动量大,上线风险极高,

该方案无法掌控;

基于上述早期方案遇到的问题,不得不寻求一种让我们改动极少,工作量可评估,可执

执行的方案,所以有了后续的做代理的设计理念,做代理这种思想算是第二种方案,主要考虑

按整个功能模块或按某个对象做代理,但是这个方案一直没有执行落地,主要是欠缺一个成熟

的可执行方案,而这个可执行方案主要目的解决一个极端重要的问题,就是如何通过这个代理

才让我们尽可能少改动代码,甚至无需改动代码,而这个方案一直没想到一个优雅的实现方案。

上面就是我们前期遇到的主要障碍,基于这样障碍与需求,我们重点考虑什么?

第一、面对一堆庞大耦合非常紧密的代码如何做业务切割时,不用为了业务切割大规模

改动现有的业务代码,能够让我们的平缓推进框架升级工作,平缓地推进业务模块测试;

第二、必须要保持现有开发人员的开发习惯,框架的变动尽可能对他们透明;

服务器旧框架示意图

旧框架概要说明,原来的框架是有一个游戏主服、N个战斗服、公共服、网关、DB

组成,概要图如下:

服务器新框架示意图

新框架概要说明,新框架包含主服、N个从服、N个战斗服、公共服、网关、DB

组成,概要图如下:

服务器新框架与旧框架之间的联系

从上述的新旧服务器框架图对比,我们可以看出,相对于旧框架,新框架仅增加了

一个新的节点类型:从服!

但是在业务功能上来说,新框架的主服已经不是旧框架的主服,旧框架下的主服几乎

承载所有游戏业务逻辑(玩家个人业务逻辑+全局业务逻辑),而现在新框架下的主服不

再承载玩家个人业务逻辑,仅仅承载全局业务逻辑(如:公会、邮件、好友、排行榜等),

而新增的从服节点负责成灾玩家个人业务逻辑,将公共的全局业务逻辑和玩家个人业务逻辑

彻底剥离开来,同时这种新框架下可将从服横向拓展,来达到我们提升承载的目的。

从服跟旧框架的主服功能几乎一样,可以正常执行玩家个人业务逻辑,从服之间可以

相互通讯,从服可以直接跟主服通讯,也可以直接跟战斗服通讯,从服可以通过网关,将

玩家均衡分配到不同的从服上,从服我们可以认为是旧框架的主服,也可以看成容纳线上

玩家的一个个房间,而这些房间不是相互封闭看不见的,而是通过上述的代理技术整合成

物理上是多房间,而逻辑上是单房间的一个概念,这样我们就可以做到在切割全局逻辑和

玩家逻辑时,无须大改动代码来达到一个拆分服务的需求;

可能大家看到这里还是有点模糊,比如说不同的从服上的玩家怎样相互感知,相互通讯?

玩家与主服的全局工会系统如何在不改动代码情况下,如何拆分在主服和从服上?这一套感觉

有点魔幻的做法,实际上都封装在代理上,以及涉及到代理广播到不同的进程上生成,如果有

兴趣,可继续耐心往下看。

新框架基于旧框架做了哪些升级改动

新框架引入的新技术:

代理技术、脚本协程

新框架的升级以及改动:涉及到网关、DB、游戏服底层、脚本层

网关升级以改动:

增加从服类;

抽象出主服类;

支持主服、从服之间可相互通讯;

支持玩家均衡登录到不同的从服;

DB升级以及改动:

增加DB主动连接到不同的从服;(玩家登录可直接将DB玩家数据加载到从服上)

游戏服底层升级以及改动:

引入python协程库;

增加网络包的保留和还原接口;(用于协程切出去后一个保存现场和还原现场)

增加网络包的引用计数;(用于网络包的保存和还原的一个生命周期管理)

增加从服编号配置;(命名为ServerSubNo,用于管理以及区分从服)

业务脚本层升级以及改动:

增加代理组件RemoteManage;

协程脚本层封装组件CoroutineWrap;

增加主服、从服跟网关通讯的通用接口:

SendPacket2GatewayCommon(nodeID, nodeType)

【主要在协议后面追加服务器编号+服务类型,用于在网关数据包的路由转发】

业务开发人员最少需要了解哪些

第一:主要需要了解怎样注册代理类;

#新增注册代理类接口
def registerdefault(typepid, callable, proxytype=None)

也就是说,如果想让业务逻辑里面某个对象类需要成为代理类,就需要通过该接口将类注册进去,

这里以工会为例子:

#引入远程代理组件
import remoteManage
#将工会类注册成代理类
remoteManage.registerdefault("CGuild", CGuild)
#将工会管理类也注册成代理类
remoteManage.registerdefault("CGuildMgr", GuildMgr.CGuildMgr)

将工会注册成代理类之后,我们怎样使用这些代理呢?下面例子给出,如果有玩家请求需要在从服上处理该请求创建工会,会调用工会模块里面的接口 Create(),下述代码主要展示如何创建工会客户端代理,示例代码如下:

# 在从服上调用工会模块下的创建工会接口
def Create(oPlayer):
    iGuildId = GenGuildID()
    import remoteManage
    # 上述通过 remoteManage.registerdefault 将 CGuild 注册成代理类后
    # 可通过下述方法去创建从服上的本地代理,下面构造的 guildObj 对象实际上
    # 本地一个代理对象,并非一个真实对象(真实对象实际上会在主服上同步构造完毕,
    # 同步构造的真实对象,又会在主服上走remoteManage.gProxyServerMng.CGuild的构造流程),
    # 但是它却能够如真实对象那样使用,请看下面demo代码
    guildObj = remoteManage.gProxyClientMng.CGuild(iGuildId, \
        dparams={"modName":guild.gLoader.dObjMap, "keyName": (iGuildId,)})
    # 调用远程真实对象的一些方法,如本地真实对象那样调用
    guildObj.CallMethod1(1,2,3, "helloworld")
    guildObj.CallMethod2({"a":1, "b":2}, "remoteCall")

上述例子代码主要展示从客户端构建代理对象,并且能同步服务端构建真实对象,接下来主要展示,如何服务端侧构建真实对象,然后会广播给各个从服同步构建代理对象,还是选择工会为例子:

Class CGuildBatchLoader():
    def __init__(self):
        # do something init
    
    def CreateObj(self, sGuildId):
        import remoteManage
        # 上述通过 remoteManage.registerdefault() 将 CGuild 注册成代理类后
        # 可通过下述方法去创建主服上的工会真实对象,这里会存储工会的真实数据以及
        # 真实管理如何将数据持久化回DB,同时会通过网关将构建该工会代理的请求广播
        # 给所有从服,也就是说在主服上构建真实对象的同时,也会在从服上同步构建该
        # 工会代理,当从服上有需要跟工会耦合的业务,则通过访问本地工会代理,本地
        # 工会代理会通过RPC调用主服上的工会真实对象提供的成员方法,完成从服上执行
        # 角色业务与工会业务的耦合的业务逻辑,也就是说原来写的业务是业务耦合的,
        # 但是为了不改动业务代码,以前业务逻辑中的工会对象,无形被替换成一个工会
        # 代理,耦合的这部分代码是无入侵改动的,但是在创建工会对象的时候,还是需要
        # 下述代码的包装,方可完成使用上述这套代理方案。
        ############################################################
        # 改造前的创建工会对象的代码如下:
        guildObj = CGuild(int(sGuildId))
        # 改造后的创建工会对象的代码如下,也就是说新框架下如果构造真实对象以及构造与之
        # 匹配的代理对象,需要依赖 remoteManage 组件来完成下述的包装改动:
        guildObj = remoteManage.gProxyServerMng.CGuild(int(sGuildId),\
            dparams={"modName":guild.gLoader.dObjMap, "keyName":(int(sGuildId),)})
        return guildObj

通过上述例子代码,我们大概了解了业务开发人员需要关心哪些组件哪些接口,就可完成业务代码的业务的拆分,从上面一些例子,我们可以知道主要我们正确构造好所需的对象以及与之匹配的代理,我们原来的全局与玩家耦合在一起的业务逻辑就可在无入侵改动代码的情况下,拆分在不同的进程上分摊压力,拿上述例子说,工会的逻辑会在主服上运算,玩家个人逻辑会在从服上运算,通过 remoteManage 组件的对一些类的包装,允许业务逻辑耦合的写法,但可根据不同的业务系统,算力可分摊到不同的进程上。

同时,解释一下上述代码中的 dparams 参数,这个参数的主要用途是,在创建远程代理的时候,需要指定这个代理对象构建完后需要摆放到本地那个模块的哪个容器里面,也是为了适配一些旧业务逻辑需要在指定的容器里面获取原来的对象,如下述代码说明:

# 可以将构造的工会代理对象,构造完毕后,将其摆放到 guild 模块的 gLoader.dObjMap 容器中
"modName": guild.gLoader.dObjMap
# 指定改工会代理对象,在 gLoader.dObjMap 中的键名
"keyName": (int(sGuildId),)

新框架的构思简单示意图

在实现这套方案的一年半前已经有这样上述代理构思,但是为什么那时还无法让构思变成可执方案落实下来?

主要是因为还没想到一个优雅的实现方案,好比已经构思好一辆车整体怎样做,但是还缺少一个汽车引擎的实现方案,没有了这个核心引擎的实现方案,就无法对整辆车进行生产行为。

但是随着服务器性能问题又尖锐起来,重新又思考这个问题的时候,无意间再深入看python的,一个多进程库multiprocess源码,里面刚刚好有一种很好的思路提供参考(所以多看开源项目代码多总结是我目前认为高效学习新技术方案的唯一方法),如何制作做这个RemoteManage代理组件,这个方案也彻底能够解决一个问题:虽然增加了这个代理的包装写法,但是无需为了切割业务去大量改动原来的业务代码,同时不用改变业务开发人员原有的开发习惯,满足上述两点,这就几乎可以实现这个代理核心引擎了。

从上述的两个树形图,在业务逻辑中我们主要是有2种情况的远程调用,第一种是如左图:按代理对象的成员方法远程调用,第二种是有图:按远程某个python 模块进行远程调用,这两种情况,都可通过更底层一点的那套rpc方案实现,上层接口做了不同封装而已。

新框架中的一些核心的实现方案

实际上,需要解决一个非常核心的问题:如何将业务逻辑原来的实体类包装成代理类?可以通过该类创建出来的对象可以是一个代理或者是一个真实对象,拿到这个对象,不管是真对象,还是代理对象,可以按原来的业务流程写代码,该怎样写就怎样写,无需任何改动任何业务代码,又拿工会作为例子:

import remoteManage
#先将原来的实体类注册,登记一下这个CGuild后续有可能使用代理包装
#(当然注册了,不用代理包装也没任何不会对CGuild创建对象不会有任何影响)
remoteManage.registerdefault("CGuild", CGuild)

#如果业务开发人员希望创建一个代理对象(必须先调用registerdefault接口将该类注册后方可使用)
guildObj1 = remoteManage.gProxyClientMng.CGuild(iGuildId, \
        dparams={"modName":guild.gLoader.dObjMap, "keyName": (iGuildId,)})

#如果业务开发人员希望创建一个真实对象(必须先调用registerdefault接口将该类注册后方可使用)
guildObj2 = remoteManage.gProxyServerMng.CGuild(int(sGuildId),\
    dparams={"modName":guild.gLoader.dObjMap, "keyName":(int(sGuildId),)

#从上面的一些讲解,我们可以知道,不管是先创建代理对象,还是先创建真实对象,
#真实对象都会有与之匹配的代理对象与之关联,它们会分布在不同的进程上,
#按照这样说的话,guildObj2作为真实对象,会在主服上被创建,
#而guildObj1作为代理对象,会在从服上创建,所以guildObj1会与guildObj2关联起来
# 举例子:如果 CGuild 类上有一个成员方法 GetGuildName() 
# 从服上写逻辑会这样写,guildObj1虽然是代理对象,跟之前旧框架写代码的方式是没有任何改变的
sName = guildObj1.GetGuildName()
# 通过代理对象调用 CGuild.GetGuildName() 成员方法,实际上在底层通过 RPC 调用了
# 远程的 guildObj2 真实对象的成员方法 GetGuildName(),实际上,在主服上执行了如下代码:
guildObj2.GetGuildName()

从上述代码,我们可以得知,关键需要实现的技术点在于:

如何实现代理类注册与封装?

在代理类的实现过程中,主要实现三个类,它们之间有如下关系:

# ProxyBaseManager 是代理封装的基类,负责实现一些代理的公共逻辑,

# 如:RPC数据包字段统一设置(nodeid、nodetype)、RPC请求统一入口处理

# ProxyClientManager 是封装客户端侧代理对象,主要实现:代理对象的构造

# ProxyServerManage 是封装服务端侧的真实对象,主要实现:真实对象的构造以及管理

构思如何封装代理类,最核心的一点就是思考如何通过构造原来的类,使创建出来的对象,对象的成员方法的行为发生改变,改变成我们想要的调用远程的真实对象的成员方法,那通过 python 我们如何做到这点呢?下面就展示我们如何做到这点,来达到我们代码几乎无入侵的升级改动:

# 这里展示的是实现原理的demo代码
# 我们如何改变一个类的构造行为呢?
# 例如: 我们当前有一个类叫 A,构造的时候,可以通过 a = A() 去构造,
#       或者这个类支持传入参数构造,如:a = A(1,2,a="hellworld")
# 那么这里的类A有点像一个函数调用,然后返回一个A类的对象,所以我们在封装代理类的时候,我们可以
# 利用这个特征,去通过创建一个与目标类同名的函数即可,实现代码如下:

# 首先我们有一个类注册成代理类的入口函数
def registerdefault(typeid, callable, proxytype=None, boOnlyServer=False, \
                    tonodeidx=-1, tonodetype=-1,delayproxy=False,lTransOpt=1):
    gProxyClientMng.register(typeid, callable, proxytype=proxytype)
    gProxyServerMng.register(typeid, callable, proxytype=proxytype, \
        nodeidx=tonodeidx, nodetype=tonodetype,delayproxy=delayproxy)
    regProxyTransFunc(callable, typeid, lTransOpt)

"""
我们需要主要关注的是 ProxyClientManager.register 的实现
从上述工会例子的,展示了 registerdefault() 接口,了解到 typeid 和 callable 的大概意义:
    typeid:传入一般是一个类的名字,可以与原来类的名字一样,也可以不一样,如 "CGuild" ,
            如果你是通过  gProxyClientMng.CGuild() 这个方式创建代理类,就必须传入
            "CGuild",如果是通过自定义类名创建代理类,如:gProxyClientMng.MyCGuild()
            这样就需要传入 "MyCGuild" 作为 typeid 参数

    callable: 从名字就可以知道,这个参数是可调用的,也就是如函数或者类等对象,这里是类,如:
            CGuild 类,因为它可以通过 oGuildObj = CGuild() 调用的方式,完成对象创建。
"""
def register(self, typeid, callable=None, nodeidx=0, nodetype=NODE_TYPE_MAIN):
    # 创建一个临时函数,来模拟类构造对象的写法
    def temp(*args, **kwds): # *args, **kwds 支持传入类构造的所有形式参数
        #首先我们需要创建一个通用代理类模版
        def MakeProxyType(name, exposed, cls):
            # exposed 是类原来的成员函数集合,下述通过遍历,去重定义这些成员函数的行为
            # 通过 exec 关键字,可让原来类的成员函数的行为变成我们设定的新成员函数:
            #     self._callmethod(), 而这个函数里面就做我们自定义的rpc行为,完成
            #     一些远程调用的行为,如函数参数序列化与协程切出切入等行为。
            dic = {}
            for meth in exposed:
                exec '''def %s(self, *args, **kwds):
                return self._callmethod(%r, args, kwds)''' % (meth, meth) in dic

            # 所有重定义后的成员函数会收集到 dic 字典里面,用于下述构造代理模版类,
            # 下述会使用了比较多 python 不太常用的用法,大家可以重点关注这部分代码
            
            # 需要让构建出来的代理模版类继承 BaseProxy, 所以增加了下述继承列表 lcls
            lcls = [BaseProxy,]
            # 构建出来的代理模版类,不单止继承 BaseProxy, 还需要基础目标类的整个继承
            # 链条,如:
            #     class CGuild(CObject, CSingleRow):
            #        原来工会需要继承 CObject, CSingleRow 等两个类
            #        那么,我们构造出来的代理模版类也需要继承这两个类
            #        总结来说,要保持原来类的继承关系,还要在最顶层基础 BaseProxy 类
            #     通过 cls.mro() 我们可以返回 CGuild 原来继承了哪些类
            # 所以这里 lcls 列表内容应该是:[CObject, CSingleRow, BaseProxy]
            lcls.extend(cls.mro())
            # 通过调用 type ,再传入指定类目 name,继承关系 lcls, 成员方法集合 dic
            # 我们就可以构建出一个与CGuild同名的ProxyType新类,而这个 ProxyType 类
            # 的成员方法名字与 CGuild 的成员方法名字一样,但是却能够重定义其行为,达到
            # 我们代理调用同名的rpc方法的效果,从而完成业务逻辑拆分,但几乎无入侵的效果
            ProxyType = type(name, tuple(lcls), dic)
            ProxyType._exposed_ = exposed
            # gdExposeCache[(name, exposed)] = ProxyType
            gdExposeCache[modName] = [ProxyType, exposed]
            
            return ProxyType

        ProxyType = MakeProxyType(typeid, exposed, cls)
        # 通过代理模版类,我们就可以创建出我们所需的代理对象,代理对象调用的所有
        # 成员方法与调用真实对象的成员方法一样,对开发人员无感知
        proxyObject = ProxyType(...) #伪代码仅用于展示,参数省略
        
        return proxyObject

    # 我们创建的 temp 函数对象,还需要为其对象重命名成,我们的实例 "CGuild"
    temp.__name__ = typeid
    # 最后我们将该 temp 函数对象,摆到 self,即 ProxyClientManager上管理起来,所以
    # 就可通过之前的示例代码去创建一个工会代理出来,如:gProxyClientMng.CGuild()
    setattr(self, typeid, temp)

通过上述核心代码的演示,我们总结可知,要创建一个代理类,我们需要构建与之对应的代理模版类,而构建代理模版类的核心是,首先如何通过exec重定义原来类的成员函数,其次如何收集原来类的继承关系,在python中,其为我们提供一个动态构造新类的函数:type(name, bases, dict),大家有兴趣可以关注一下这个函数的用法,这里就不具体展开描述该函数用法,仅仅通过上述示例代码展现如何封装一个代理类的核心代码。

如何封装底层greenlet协程库以及相关接口使用

注意:使用 greenlet这个库之前,需要在游戏C++层引入 greenlet 库

python 层对 greenlet 库进行了简单的封装,封装主要目的是做一些协程调用的性能统计以及协程的接入切出接口,让在python层更加方便使用协程。

#协程封装
class Greenlet(greenlet.greenlet):
    def __init__(self, func, iUID=None):
        greenlet.greenlet.__init__(self, func)
        if None <> iUID:
            self.iUID = iUID
        else:
            self.iUID = NewCoGUID()
        self.typeid = ""     #当前协程执行的类名
        self.methname = ""    #当前协程执行的成员函数(或者普通函数)
        self.args = ""        #当前协程执行函数的参数元组
        self.kwds = ""        #当前协程执行函数的参数字典
        self.runcost = 0     #当前协程执行耗时(纳秒)
        self.waitcost = 0     #当前协程等待唤醒耗时(纳秒)
        self.runtotal = 0     #当前协程完成业务运行总时间(纳秒)
        self.waittotal = 0     #当前协程完成业务挂起等待总时间(纳秒)
        self.starttime = 0     #开始统计开始时刻
        self.SetStartTime()
        self.proto = 0
        self.wait = False
        self.stack = ""
        self.ret = ""

    def UID(self):
        return self.iUID

    def Remove(self):
        self.SetStatInfo("Greenlet", "Remove", "", "")

    def GetWaitFlg(self):
        return self.wait

    def CleanStatInfo(self):
        self.typeid = ""     #当前协程执行的类名
        self.methname = ""    #当前协程执行的成员函数(或者普通函数)
        self.args = ""        #当前协程执行函数的参数元组
        self.kwds = ""        #当前协程执行函数的参数字典
        self.runcost = 0     #当前协程执行耗时(纳秒)
        self.waitcost = 0     #当前协程等待唤醒耗时(纳秒)
        self.stack = ""
        self.ret = ""

class CoroutineWrap(object):
    def __init__(self):
        self.dCorMng = {}
        self.dCorChkTime = {}
        self.oldcallback = greenlet.settrace(callback)
        self.statistime = GetSecond()
        self.statisMap = {}

    # 创建协程实例,返回协程ID
    def AddCo(self, func):
        oNewCo = Greenlet(func)
        iCoId = oNewCo.UID()
        self.dCorMng[iCoId] = oNewCo
        return iCoId

    # 通过协程ID删除协程实例
    def DelCo(self, iCoId):
        oCo = self.GetCo(iCoId)
        if None == oCo:
            raise Exception(Language("协程对象不存在,无法执行DelCo,
                    iCoId=%0$s,oCo=%1$s",iCoId,str(oCo)))
        if not oCo.dead:
            raise Exception(Language("协程还未Dead,无法执行DelCo,
                    iCoId=%0$s,oCo=%1$s",iCoId,str(oCo)))
        oCo.Remove()
        del self.dCorMng[iCoId]
        if self.dCorChkTime.has_key(iCoId):
            del self.dCorChkTime[iCoId]

    # 将主干根协程包装成Greenlet类对象
    def InitTrunkCo(self):
        oTrunkCo = greenlet.getcurrent()
        # 主干协程是C++底层的0号主协程,无法被 Greenlet 类包装起来
        # 所以这里需要判断是否有 iUID 属性来判断是否为0号主协程
        if not hasattr(oTrunkCo, "iUID"):
            setattr(oTrunkCo, "iUID", 0)
            setattr(oTrunkCo, "proto", -1)

    # 切协程:有当前执行的协程切到目标协程:oTarCo
    def SwitchCo(self, oTarCo, *args, **kwds):
        try:
            return oTarCo.switch(*args, **kwds)
        except:
            LogPyException()

#    def HasCo(self, iCoId):
#        return True if self.GetCo(iCoId) else False

    #★★获取出去的协程对象不能直接用 if oCo 去判断,因为这样判断是判断oCo.dead
    def GetCo(self, iCoId):
        return self.dCorMng.get(iCoId)

    def IsDead(self, iCoId):
        oCo = self.GetCo(iCoId)
        return oCo.dead

    # 通过协程执行网络入口执行的业务函数 func,以及对应的参数 args
    def MainRun(self, func, *args):
        return self.Resume(self.AddCo(func), *args)

    # 包装客户端请求处理函数 topfunc 以及对应的参数 args
    def WrapRunCmd(self, topfunc, *args):
        self.InitTrunkCo()
        return self.MainRun(topfunc, *args)
        
    # Yield起来,实际上从当前执行业务协程切回0号主协程的流程
    def Yield(self, *args, **kwds):
        oCurCo = greenlet.getcurrent()
        coid = oCurCo.iUID
        if 0 == coid:
            raise Exception, Language("当前执行OnNetPacketCall
                                        主干流程还没被根协程封装好!")
        if None == self.GetCo(coid):
            raise Exception, Language("当前执行OnNetPacketCall
                                        主干流程的协程不在管理器内!")
        # 创建对应协程定时器,监控协程是否唤醒,避免回包异常,无法正常唤醒
        # 该协程,导致大量睡眠协程,导致 python 层内存泄漏
        timerkey = "timerproxyco%s"%coid
        if FindCallLater(timerkey): 
            DelCallLater(timerkey)
        CallLater(CFunctor(self.Resume, coid, "timeout", coid), 20, timerkey)

        return self.SwitchCo(oCurCo.parent, *args, **kwds) #切回去主流程的协程

    # Rusume恢复协程,实际上拿出需要执行的协程继续执行
    def Resume(self, iCoId, *args, **kwds):
        sTimerKey = "timerproxyco%s"%iCoId
        if FindCallLater(sTimerKey):
            DelCallLater(sTimerKey)

        oCo = self.GetCo(iCoId)
        oCo.parent = greenlet.getcurrent() # 记录恢复之前的母协程
        result = self.SwitchCo(oCo, *args, **kwds) #切回去挂起的协程
        
        if oCo.dead: self.DelCo(iCoId)
        return result

上面就是协程封装的演示代码,通过协程的睡眠与唤醒,让我们异步的执行的业务代码,用上了同步的写法,达到让我们的业务上层代码无需类似callback那样分段写,从另外一个角度达到无入侵改动代码达到切割业务代码的效果。

如何封装远程调用RPC以及各种参数序列化和反序列化

这里也需要通过一些实例代码,加上注释去展示如何实现这些过程:

# 所有代理的远程rpc都会通过下述接口进行远程调用
def dispatch_managerclass(mng, methodname, args=(), kwds={}, nodeidx=-1, \
                    nodetype=-1, boWait=True, typeid="", gatewayobj=None):
    # 远程调用的一些参数序列化 (使用json或者bison,这里就不展开说)
    args, kwds = _params_encode(args, kwds)
    # 获取当前执行业务的协程对象
    oCo = greenlet.getcurrent()
    coid = oCo.iUID

    # 缓存当前的接受或者发送包到底层,协程执行完毕后恢复
    sArgs, sKwds = str(args), str(kwds)
    # 通过 mng 的类型,判断使用不同的子协议
    sub = SUB_CALL_REMOTECALL if isinstance(mng, ProxyClientManager) \
                                            else SUB_CALL_REMOTECALL2
    
    # 如果当前 rpc 需要等待返回数据包,则需要将当前的客户端的数据包与当前协程ID绑定
    if boWait: BindPacket2Coid(coid)

    # 构造远程调用的请求数据包:包头、子协议号、调用函数或者成员函数名字、参数
    try:
        ProxyBaseManager.MakeProxyPacketHead(sub, coid, gatewayobj)
        C_PacketAppendS('-1')                      #对象编号
        C_PacketAppendI(-1, 8)                     #对象UID
        C_PacketAppendS(typeid)                    #对象类名
        C_PacketAppendS(methodname)                #调用方法
        sDumpText,iEncode=localdumps((args, kwds,))
        C_PacketAppendS(sDumpText)                 #调用方法参数
        C_PacketAppendI(iEncode, 1)                #编码类型(json或bsion)
        C_PacketAppendI(boWait, 1)                 #告知远程被调用方是否返回数据
        
        import Trans; 
        Trans.SendPacket2GatewayCommon(nodeidx, nodetype)
    except:
        # 如果需要等待返回包,出现异常时,则需要通过当前协程ID恢复上一次的客户端数据包环境
        if boWait: RestorePacket2Coid(coid)
        LogPyException()
        raise Exception(Language("dispatch_managerclass远程请求失败!"))

    if boWait:
        # 如果需要等待返回数据包,则需要通过协程挂起,暂时切出当前业务流程,
        # 让游戏服底层可处理其他网络数据包(游戏服底层处理业务逻辑是单线程)
        coret, lastcoid = gCoMng.Yield()

        #入口函数已经将sub和coid解析出来,这里从解析fromnodeidx,fromnodetype开始
        if "normal" == coret:
            try:
                fromnodeidx, fromnodetype = ProxyBaseManager.ParseProxyPacketHead()
                addrid = C_UnpackStr()
                uid = C_UnpackInt(8)
                backtypeid = C_UnpackStr()
                backfuncname= C_UnpackStr()
                sParam = C_UnpackStr()
                msg = localloads(sParam)
                C_ReleasePacket(RECV_TYPE_PACKET) #先删除返回的唤醒协程包
                RestorePacket2Coid(coid) #再恢复协程切出去之前的底层包环境
            except:
                msg=""
                C_ReleasePacket(RECV_TYPE_PACKET)
                RestorePacket2Coid(coid); LogPyException()
                raise Exception(Language("dispatch_managerclass远程返回失败!"))

            # 反序列化远程调用后回来的数据 (使用json或者bison,这里就不展开说)
            msg, _ = _params_decode(msg)
            kind, result = msg
            sResult = str(result)

            if kind == '#RETURN':
                return result

        # 远程调用返回超时,定时器超时唤醒当前协程
        elif "timeout" == coret:
            RestorePacket2Coid(coid)
            raise Exception(Language("协程%0$s超时,结束当前业务流%1$s.%2$s,
                args=%3$s,kwds=%4$s!",coid,typeid,methodname,sArgs,sKwds,))

        # 远程调用返回,其他未知异常(实际上仅仅是 default 处理而已)
        else:
            RestorePacket2Coid(coid)
            raise Exception(Language("协程%0$s异常唤醒,结束当前业务流%1$s.%2$s,
                args=%3$s,kwds=%4$s!",coid,typeid,methodname,sArgs,sKwds))
    else:
        pass

# 根据当前协程ID保存底层数据包现场
def BindPacket2Coid(coid):
    C_BindPacket(coid, RECV_TYPE_PACKET)
    C_BindPacket(coid, SEND_TYPE_PACKET)

# 根据协程ID还原底层数据包现场
def RestorePacket2Coid(coid):
    C_RestorePacket(coid, RECV_TYPE_PACKET)
    C_RestorePacket(coid, SEND_TYPE_PACKET)

从上面的实例代码,我们可以看出这些代码不具备通用性,因为要依赖项目框架的一些网络压包接口才能完成网络数据包的发送,从而在底层完成不同服务节点之间的通讯,所以这里仅仅展示一些实现远程调用的例子方案而已,如果涉及到具体的游戏项目,需要调用具体项目的一些网络数据包接口,也能达到相同的效果。

这里还需要额外解释一下,上述代码两个游戏服底层提供的三个接口:

C_BindPacket ()和 C_RestorePacket()、C_ReleasePacket()

在没有进行框架升级之前,底层只有一块内存用于接收当前正在处理的数据包,业务执行完毕后,下一个的网络数据包内容将覆盖这块内存,在新框架的引入了协程的会导致大量业务依赖异步处理,也就是说,在业务执行过程中,协程切出去,再接收处理其他数据包的时候,新的数据包就会覆盖底层这块内存,当远程调用数据包返回唤醒原来的协程时,原来的协程还需要依赖原来的数据包环境,所以我们这里需要做的一件额外事情就是要在游戏底层需要提供一种数据包的保存和还原现场的机制,所以就有了上述接口。(备注:实际上有些游戏服框架如果底层将数据包直接拷贝到脚本层,让脚本层的协程环境去hold住这个数据包,就不需要在底层去实现这种数据包的现场保存和恢复机制)

新框架的一些基础组件的开源

该框架升级涉及改动升级的地方比较多,涉及到游戏服C++底层改动、网关层的改动、新增业务层的RemoteManage 远程管理组件,这里主要开源这个组件的实现代码,以展示一些在python下做代理对象的思路,无入侵式改动业务代码来达到切割业务分服的目的

开源代码路径:(python源码仅提供一些做代理的思路,做rpc这块由于使用了项目特定的网络接口,所以如果在其他项目还需要根据自己项目做定制)

https://gitee.com/kxbwiner/py-util

大家有什么想法可以留言,后续可以继续完善该开源项目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值