MMORPG服务器的业务钩子

  很多MMORPG类游戏的开发都使用了动态语言,比如lua、python等,集成动态语言最明显的好处是可以热更新,在日常维护中,经常需要修正一个业务逻辑错误,或者修改一个配置数据项,又不想重启服务,那么动态刷新代码就成了刚需了;而动态语言解释执行代码的机制就成了刚需的不二选择。但是大部分服务器的框架甚至主要业务可能还是由静态语言编写的,比如C、C++等,虽说这些核心代码都经过大量的测试,质量可能非常高,但漏洞终归是有的,有时候核心代码出了问题,又不能重启服务,可是静态语言的机制又不支持动态刷新,这个时候有什么办法能再一次的实现这个“刚需”呢?

  在我参与的几个项目中,这个需求确实是个刚需!项目部对lua程序员犯错的容忍性比较高,但也从不假设C++程序员不犯错。每次遇到C++的业务逻辑错误,基本上就是重启服务,似乎大家已经接受“重启”是一种标准解决方案了。本文讨论的“钩子”,很大程度上解决了这个问题,在实际应用中效果还不错,服务“重启”的必要性已经大大降低了。

1. 钩子

  本文所谓的“钩子”,就是一个dll,事先在服务器中安置好“驱动”这个dll的代码,在需要时,可以加载/运行/卸载这个dll,暂且将这个dll命名为ServerHooker.dll吧。先看下代码:

bool ServerCenter::LoadServerHooker()
{
    // serverHooker_是IServerHooker类型的指针,其派生类ServerHooker是业务钩子类,该类和
    // ServerCenter中其它所有业务类类似,对于业务中心来说,ServerHooker就是一个普通的业务类
    if (serverHooker_ != nullptr) return false;

    // "ServerHooker"是dll名字,"CreateHookServer"是创建ServerHooker类实例的函数,原型为:
    // typedef IServerHooker* ProcHookServer(void*);
#ifdef WIN32
    HMODULE handle = ::LoadLibrary("ServerHooker");
    if (!handle) return false;
	
    FARPROC func = GetProcAddress(handle, "CreateHookServer");
    if (!func)
    {
        ::FreeLibrary(handle);
        return false;
    }
	
    serverHooker_ = ((ProcHookServer*)func)((void*)handle);
    if (serverHooker_ == nullptr)
    {
        ::FreeLibrary(handle);
        return false;
    }
#else
    void* handle = dlopen("ServerHooker", RTLD_NOW);
    if (!handle) return false;
	
    void* func = dlsym(handle, "CreateHookServer");
    if (!func)
    {
        dlclose(handle);
        return false;
    }
	
    serverHooker_ = ((ProcHookServer*)func)((void*)handle);
    if (serverHooker_ == nullptr)
    {
        dlclose(handle);
        return false;
    }
#endif

    return true;
}

以上代码负责加载业务钩子并初始化。接着,提供一个运行钩子的接口:

bool ServerCenter::RunServerHooker()
{
    if (serverHooker_)
        return serverHooker_->Run();

    return false;
}

最后是释放业务钩子:

bool ServerCenter::UnloadServerHooker()
{
    if (serverHooker_ == nullptr)
        return false;

    void* handle = serverHooker_->GetHandler();
    serverHooker_->Release();
    serverHooker_ = nullptr;
	
#ifdef WIN32
    ::FreeLibrary((HMODULE)handle);
#else
    dlclose(handle);
#endif

    return true;
}

  钩子的驱动接口都准备妥当,这些接口可以“动态”调用,意思是说,不用重启服务,通过脚本刷新或者发消息进行调用。钩子加载进业务中心之后,是否运行(Run)或者卸载(Unload),要看具体的问题是什么性质。有的问题是“一锤子”买卖,比如要修改某个业务模块中的一个数据成员,则加载钩子之后,就可以立即运行执行修改,修改之后即可卸载;而有的问题需要监控一些状态的变化,或者侦听一些指定的消息,那么钩子就可能需要一直运行到下次维护。接下来讨论业务钩子的用法。

2. 应用

2.1. 一锤子买卖

  考察“商城”系统,假设商城系统中有一个叫“道具兑换折扣”的量表示在商城用某一种资源兑换道具的折扣,根据服务器开服时长、上周参与兑换的人数、上周道具库存等各种指标在服务器启动后初始化商城系统时计算得出:

class Mall : public IMall
{
private:
    void CalculateDiscount();
private:
    double   discount_;
};

再假设版本上线之后,发现这个折扣率计算错误,导致折扣过高,比如是6.5折,而正确值应该是8.5折,模块中没有为该折扣率提供任何可修改它的接口暴露给外界使用。这种情况用钩子比较好解决。步骤如下:

  1、先在Mall类中声明ServerHooker类为其友元类:

class Mall : public IMall
{
    friend class ServerHooker;
private:
    void CalculateDiscount();
private:
    double   discount_;
};

  2、然后在ServerHooker类中,通过ServerCenter取到Mall的句柄,一般情况下,ServerCenter中的所有业务都有一个句柄作为具体业务的入口:

class ServerHooker : public IServerHooker
{
private:
    virtual void Run()
    {
        IMall* mallHandler = ServerCenter::GetMallHander();
        if (!mallHandler) return;
        Mall* mall = dynamic_cast<Mall*>(mallHandler);
        if (!mall) return;
        
        mall->discount_ = 0.85;
    }
};

  3、接着编译出ServerHooker.dll,并使用前面讨论的事先暴露出来的钩子接口,将该dll加载进ServerCenter;
  4、运行ServerCenter::RunServerHooker,从而执行ServerHooker::Run函数,完成修正。

  注意:不需要编译Mall模块,虽然修改了Mall.h(在Mall类中增加了友元声明),但这仅仅是为了在编译ServerHooker模块代码时绕开编译器的限制,因为ServerHooker中访问了Mall对象的私有变量discount_,我们只需要骗过编译器就行

2.2. 持续监控

  再次考察“商城”系统,假如玩家在商城兑换道具之时,由于代码错误,给玩家发放道具时发了两份:

class Mall : public IMall, public NetEventHandler
{
public:
    bool Init()
    {
        ......
        ServerCenter::RegisterNetEvent(MALL, this); // 注册网络事件观察
    }
private:
    virtual void OnMessage(DWORD msgCode, BYTE* msg, int len)
    {
        switch (msgCode) 
        {
        case EXECHANGE:
            OnExechange(msg, len);
            break;
        ...
        default:
            break;
        }
    }
    
private:
    double   discount_;
};

一般地设计,ServerCenter中的各业务模块初始化时会向ServerCenter注册网络事件,以便有网络消息到达时,ServerCenter可以将消息转发到各业务模块。针对这个问题,思路是这样:首先接受这个错误,然后纠正这个错误。做法是这样的:

  1、首先,向ServerCenter注销Mall模块的网络事件观察,并代替Mall模块向ServerCenter注册Mall模块的网络事件观察:

class ServerHooker : public IServerHooker, public NetEventHandler
{
private:
    virtual void Run()
    {
        IMall* mallHandler = ServerCenter::GetMallHander();
        if (!mallHandler) return;
        ServerCenter::UnregisterNetEvent(MALL, mallHandler); // 注销Mall模块的网络事件观察
        ServerCenter::RegisterNetEvent(MALL, this); // 代替Mall模块注册网络事件观察
    }
};

  2、接着在ServerHooker::OnMessage中,接管Mall模块的网络消息,因为只有兑换(EXECHANGE)有问题,所以只处理兑换消息,其它的还是直接转发给Mall模块:

class ServerHooker : public IServerHooker, public NetEventHandler
{
private:
    virtual void OnMessage(DWORD msgCode, BYTE* msg, int len)
    {
        IMall* mallHandler = ServerCenter::GetMallHander();
        NetEventHandler* mall = dynamic_cast<NetEventHandler*>(mallHandler);
        if (mall == nullptr) return;
	    
        switch (msgCode) 
        {
        case EXECHANGE:
            mall->OnMessage(msgCode, msg, len); // 先让Mall模块处理兑换道具
            RemoveGoods(msg, len); // 然后从玩家包裹中删除多余的道具
            break;
        ...
        default:
            mall->OnMessage(msgCode, msg, len); // 其它的消息直接转发
            break;
        }
    }
};

3. 结语

  业务钩子比较擅长修改数据,而修改逻辑通常是比较难的,尤其是很复杂的逻辑。比如某个函数内部的局部缓冲区(如临时数组)的长度太小,这种是很难改变的。如果复杂的逻辑中有依赖全局、静态或者数据成员变量,那么可以通过修改这些数据来改变逻辑的运行轨迹,从而达到部分修改逻辑的目的。比如某函数中某个地方出错,但这个地方很难直接修改,为了将问题严重性降到最低,需要后续的代码不要继续执行,而后续的代码是一个条件分支,依赖某个非局部变量的值,那么我们就可以修改这个变量,让后续的代码跳过不想执行的分支。

  修改逻辑比修改数据更难,也有机制上的原因。很显然,本文讨论的解决方案对被修改的模块(目标模块)都是侵入性的,不管是直接修改目标模块的数据,还是调整目标模块的逻辑,都是要进入到目标模块内部的;假设目标模块也是被封装在一个dll(目标dll)之内,那么除了虚函数或被导出的静态函数这些直接对外暴露的接口可以被直接调用外,其它像类的普通成员函数,未被导出的静态函数可能无法被直接调用到,有时候可能会采用非常规的手段以获取目标函数地址,至于有多么非常规,我也不知道。

  业务钩子不是万能的,静态语言毕竟不是解释性的语言,想要达到动态语言的那种随心所欲的动态刷新,是不可能的。怎么解决问题或者规避问题,还是比较烧脑的,但办法从来都不止一种!业务钩子,至少提供了一种思路,有时候它甚至是最后的救命稻草!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值