首页 > 运营推广 > 详情

开源服务器框架NoahFrame分享:第二章 插件与模块

最初接触插件(Plugin)是当年开发客户端的时候使用的Ogre引擎,里面的设计另当时我这个小菜鸟惊叹不己,原来还可以这样组织代码。然后时隔多年,在Ogre的影响下,又进一步在NF引擎内加入了module和component,用以完善插件式遗漏的一些缺陷。

Ogre的插件式架构,是建立在动态库上的,windows为.dll,linux为.so,NF引擎也是如此(我在主导无双项目在开发的时候,又全部改成了静态库,改成静态库在NF引擎中只需要修改几十行代码即可)。使用插件来组织代码,好处非常多,比如同事分工协作方面,比如逻辑热更新方面(静态语言非脚本,比如使用c 热更新),比如维持代码的单纯度和统一管理规范方面,比如企业安全信息保密方面等等。

NF引擎的插件管理比Ogre略复杂,主要体现在每个插件内部都有module,然后所有的module在启动时又都注册到PluginManager接受PluginManager的管理。NFPluginLoader为程序的执行入口,他会自动查找启动目录下的Plugin.xml文件,然后加载里面配置过的plugin(或者自行传入名字让PluginLoader加载),例如:

<XML>

        <GameServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />

                <Plugin Name="NFGameServerPlugin" />

                <Plugin Name="NFGameServerNet_ServerPlugin" />

                <Plugin Name="NFGameServerNet_ClientPlugin" />

                <Plugin Name="NFLogPlugin" />

            <ConfigPath Name="../" />                

        </GameServer>


        <LoginServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />        

                <Plugin Name="NFLoginLogicPlugin" />

                <Plugin Name="NFLoginNet_ServerPlugin" />

                <Plugin Name="NFLoginNet_ClientPlugin" />

                <Plugin Name="NFLogPlugin" />

            <ConfigPath Name="../" />                

        </LoginServer>


        <MasterServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />

                <Plugin Name="NFMasterServerPlugin" />

                <Plugin Name="NFMasterNet_ServerPlugin" />

                <Plugin Name="NFMasterNet_HttpServerPlugin" />

                <Plugin Name="NFLogPlugin" />

            <ConfigPath Name="../" />                

        </MasterServer>


        <ProxyServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />

                <Plugin Name="NFProxyLogicPlugin" />

                <Plugin Name="NFProxyServerNet_ClientPlugin" />

                <Plugin Name="NFProxyServerNet_ServerPlugin" />

                <Plugin Name="NFLogPlugin" />

            <ConfigPath Name="../" />                                

        </ProxyServer>


        <WorldServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />

                <Plugin Name="NFWorldNet_ClientPlugin" />

                <Plugin Name="NFWorldNet_ServerPlugin" />

                <Plugin Name="NFLogPlugin" />        

            <ConfigPath Name="../" />                

        </WorldServer>

        

        <TutorialServer>

                <Plugin Name="NFKernelPlugin" />

                <Plugin Name="NFConfigPlugin" />

                <Plugin Name="NFLogPlugin" />        

                <Plugin Name="Tutorial1" />        

            <ConfigPath Name="../" />                

        </TutorialServer>

</XML>


Plugin.xml内部声明了每类服务器启动的时候需要加载的插件以及配置文件(NFDataCfg)路径,因为考虑到产品运维更偏向使用脚本批量启动服务器,因此AppID在脚本中可以传入,比如:


./NFPluginLoader_d -d Server=MasterServer ID=3

./NFPluginLoader_d -d PluginX.xml Server=MasterServer ID=3


插件加载程序的入口在文件NFPluginLoader.cpp的main函数,会先初始化NFCPluginManager,

然后调用NFCPluginManager进行初始化加载动态库(.dll  .so),然后统一管理所有的插件,并统一进行初始化,反初始化,帧执行等操作,简略代码如下:


int main(int argc, char* argv[])

{

        ProcessParameter(argc, argv);


        NFCPluginManager::GetSingletonPtr()->Awake();

        NFCPluginManager::GetSingletonPtr()->Init();

        NFCPluginManager::GetSingletonPtr()->AfterInit();

        NFCPluginManager::GetSingletonPtr()->CheckConfig();

        NFCPluginManager::GetSingletonPtr()->ReadyExecute();


        while (true)

        {

                std::this_thread::sleep_for(std::chrono::milliseconds(1));


                NFCPluginManager::GetSingletonPtr()->Execute();

        }


        NFCPluginManager::GetSingletonPtr()->BeforeShut();

        NFCPluginManager::GetSingletonPtr()->Shut();


        NFCPluginManager::GetSingletonPtr()->ReleaseInstance();


        return 0;

}


因为任何一个插件(Plugin)必须继承自NFIPlugin类,它拥有NFIModule类所有的统一调用接口(任何模块,必须继承自NFIModule并拥有以下接口Awake, Init, AfterInit, Execute, BeforeShut, Shut,  Finalize等统一接口):

class NFIModule

{


public:

        virtual bool Awake()

        {

                return true;

        }


        virtual bool Init()

        {

                return true;

        }


        virtual bool AfterInit()

        {

                return true;

        }


        virtual bool CheckConfig()

        {

                return true;

        }


        virtual bool BeforeShut()

        {

                return true;

        }


        virtual bool Shut()

        {

                return true;

        }


        virtual bool ReadyExecute()

        {

                return true;

        }


        virtual bool Execute()

        {

                return true;

        }

};


这些函数被调用的顺序,和NFCPluginManager启动的时候调用的顺序是一样的,每一个继承NFIModule的类(Mudole),都是按照此顺序运行。


所有的Module类的载体都是插件(动态库),因此为了夸平台,首先NF对要加载的动态库作了一个抽象,用NFCDynLib类来表示(这里向Ogre的作者致敬,几乎纯抄他的),一个动态库就是一个NFCDynLib类对象。同时NFCPluginManager类用来管理所有加载的NFCDynLib类对象,它负责根据动态库文件名对相应的库进行加载,并保存加载后的NFCDynLib对象指针,对于不同平台的插件区分加载,也是这里实现的,代码大概如下:

class NFCDynLib

{


public:


        NFCDynLib(const std::string& strName)

        {

                mbMain = false;

                mstrName = strName;

#ifdef NF_DEBUG_MODE

                mstrName.append("_d");

#endif


#if NF_PLATFORM == NF_PLATFORM_WIN

                mstrName.append(".dll");

#else

  mstrName.append(".so");

#endif


                printf("LoadPlugin:%s ", mstrName.c_str());

        }


        ~NFCDynLib()

        {


        }


        bool Load()

        {

                std::string strLibPath = "./";

                strLibPath = mstrName;

                mInst = (DYNLIB_HANDLE)DYNLIB_LOAD(strLibPath.c_str());


                return mInst != NULL;

        }


        bool UnLoad()

        {

                DYNLIB_UNLOAD(mInst);

                return true;

        }


        const std::string& GetName(void) const

        {

                return mstrName;

        }


        const bool GetMain(void) const

        {

                return mbMain;

        }


        void* GetSymbol(const char* szProcName)

        {

                return (DYNLIB_HANDLE)DYNLIB_GETSYM(mInst, szProcName);

        }


protected:


        std::string mstrName;

        bool mbMain;


        DYNLIB_HANDLE mInst;

};


然后NFCPluginManager::Awake()函数在调用的时候,会先调用LoadPluginConfig()函数来动态库通过查找Plugin.xml内部配置的插件列表来加载需要的插件:

bool NFCPluginManager::LoadPluginConfig()

{

    std::string strContent;

    GetFileContent(mstrConfigName, strContent);


    rapidxml::xml_document<> xDoc;

    xDoc.parse<0>((char*)strContent.c_str());


    rapidxml::xml_node<>* pRoot = xDoc.first_node();

    rapidxml::xml_node<>* pAppNameNode = pRoot->first_node(mstrAppName.c_str());

    if (!pAppNameNode)

    {

        NFASSERT(0, "There are no App ID", __FILE__, __FUNCTION__);

        return false;

    }


    for (rapidxml::xml_node<>* pPluginNode = pAppNameNode->first_node("Plugin"); pPluginNode; pPluginNode = pPluginNode->next_sibling("Plugin"))

    {

        const char* strPluginName = pPluginNode->first_attribute("Name")->value();


        mPluginNameMap.insert(PluginNameMap::value_type(strPluginName, true));


    }


    rapidxml::xml_node<>* pPluginConfigPathNode = pAppNameNode->first_node("ConfigPath");

    if (!pPluginConfigPathNode)

    {

        NFASSERT(0, "There are no ConfigPath", __FILE__, __FUNCTION__);

        return false;

    }


    if (NULL == pPluginConfigPathNode->first_attribute("Name"))

    {

        NFASSERT(0, "There are no ConfigPath.Name", __FILE__, __FUNCTION__);

        return false;

    }


    mstrConfigPath = pPluginConfigPathNode->first_attribute("Name")->value();


    return true;

}


其次会开始执行Init AfterInit等函数,每个函数都会调用内部NFIPlugin的同名接口,而在NFIPlugin内部又会调用所有的Module同名接口。比如Init函数做实例:


调用NFCPluginManager::Init内部会迭代所有的NFIPlugin,来调用NFIPlugin同名Init函数:

virtual bool NFCPluginManager::Init()

{

    PluginInstanceMap::iterator itInstance = mPluginInstanceMap.begin();

    for (itInstance; itInstance != mPluginInstanceMap.end(); itInstance )

    {

        itInstance->second->Init();

    }


    return true;

}



//而NFIPlugin内部又回迭代所有的Module,调用Module同名Init函数:

virtual bool NFIPlugin::Init()

    {

        NFIModule* pModule = First();

        while (pModule)

        {

            bool bRet = pModule->Init();

            if (!bRet)

            {

                assert(0);

            }


            pModule = Next();

        }



当所有的初始化成功之后,接着就会开始帧循环,以便网络库,心跳库等能够有机会处理自己的逻辑(比如接受消息,然后调用各逻辑模块驱动逻辑进行;又比如到点发奖等内容)代码如下,是不是和Init几乎一样?:


virtual bool NFCPluginManager::Execute()

    {

        NFIModule* pModule = First();

        while (pModule)

        {

            pModule->Execute();


            pModule = Next();

        }


        return true;

    }


  virtual bool NFIPlugin::Execute()

    {

        NFIModule* pModule = First();

        while (pModule)

        {

            pModule->Execute();


            pModule = Next();

        }


        return true;

    }


在NF的插件架构中,核心在于module的驱动(因为所有的业务逻辑都在module内),插件plugin只是module的载体。当module实例化后,和插件几乎没什么关系,他们可以跨插件使用其他module而不存在限制,因为NF是面向接口编程,任何module只需依赖其他module的接口(请自行观看各种设计模式书籍)。



那么,如何在一个module中来获取其他module呢?



每个module初始化的时候,都会调用Awake, Init, AfterInit, ReadyExecute 函数,这几个函数一般用来做什么呢,为了大家统一理解,一般作为如下用途:

NFIModule::Awake(): 主要用来初始化自身资源。比如要对其他module提供的服务,比如提供一些随机数池,预生成各种对象等;

NFIModule::Init(): 主要初始化自身需要的一些其他Module的接口,通过pPluginManager->GetModule<NFIModule>()函数来获得其他module的接口,NFIModule请自行替换成你自己写的逻辑类(在NF的世界中,只有的业务逻辑module都建议以NFI 或者 NFC开头,否则在find的时候可能会失败);

NFIModule::AfterInit(): 获取到其他Module接口后,可能依赖其他Module的资源做一些自身的初始化。比如启动网络库开放端口,添加网络MsgID的观察者,添加LogicClass的观察者等;NFIModule::ReadyExecute(): 此函数主要用来在于网络消息处理器的猎夺处理功能,我们希望一些逻辑在新才Module处理,而旧的Module却也不删除,则用可以考虑在此增加代码(参考LoginServer对于登陆消息的猎夺处理)


这里可能有同学比较在意业务动态更新,想使用lua等脚本语言。当然,NF本身是支持lua插件的,但是既然是动态库,插件也可以更新,那么理论上就可以热更新逻辑和配置数据了。


NF是基于Plugin来组织代码,基于Module来提供业务接口,因此可以用GM命令通过NFCPluginManager::ReLoadPlugin(const std::string& strDllName) 重新加载插件。

当插件加载完毕后,会同步通知所有的module,有新插件加载,此时NFIModule::OnReloadPlugin函数,会自动得到回调,使用者可以在OnReloadPlugin重新初始化所有的接口即可,而不用关心其他内容。


NF项目为开源的分布式服务器解决方案,其中包含了网络库,actor库,以及数据驱动等新技术,能大幅提升开发效率节省开发周期以及提高程序的稳定性。

项目地址 https://github.com/ketoo/NoahGameFrame


QR code