当前位置: 首页 > 工具软件 > NoahFrame > 使用案例 >

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

商飞翮
2023-12-01

NF最早为客户端设计,后来随着时代的变化,而为自己又转为服务器开发,故在吸收了众多引擎的优点后(包含Ogre的插件模式&模块化管理机制,Bigworld的数据管理&配置机制,类似MYGUI的接口层次设计),经过多年演化和实践,变成了一套游戏开发J解决方案。方案中包含开源的服务器架构,网络库(站在libevent的肩膀上),和unity3d的demo源码。现在NF已经在多个公司的多个项目中使用,其中包含知名产品 《全民无双》。

关键词:

NoahGameFrame/NoahFrame/NF
集群/负载均衡/分布式
网关服务器 GateServer 心跳 多线程/线程池 开源网络框架/模型
一致性hash算法/ConsistentHash
游戏开发中的设计模式/数据结构
Socket Nagle/粘包/开源游戏服务器/ Game Server

最初接触插件(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加载),例如:
 

  1. <XML>
  2.         <GameServer>
  3.                 <Plugin Name="NFKernelPlugin" />
  4.                 <Plugin Name="NFConfigPlugin" />
  5.                 <Plugin Name="NFGameServerPlugin" />
  6.                 <Plugin Name="NFGameServerNet_ServerPlugin" />
  7.                 <Plugin Name="NFGameServerNet_ClientPlugin" />
  8.                 <Plugin Name="NFLogPlugin" />
  9.             <ConfigPath Name="../" />               
  10.         </GameServer>
  11.         <LoginServer>
  12.                 <Plugin Name="NFKernelPlugin" />
  13.                 <Plugin Name="NFConfigPlugin" />        
  14.                 <Plugin Name="NFLoginLogicPlugin" />
  15.                 <Plugin Name="NFLoginNet_ServerPlugin" />
  16.                 <Plugin Name="NFLoginNet_ClientPlugin" />
  17.                 <Plugin Name="NFLogPlugin" />
  18.             <ConfigPath Name="../" />               
  19.         </LoginServer>
  20.         <MasterServer>
  21.                 <Plugin Name="NFKernelPlugin" />
  22.                 <Plugin Name="NFConfigPlugin" />
  23.                 <Plugin Name="NFMasterServerPlugin" />
  24.                 <Plugin Name="NFMasterNet_ServerPlugin" />
  25.                 <Plugin Name="NFMasterNet_HttpServerPlugin" />
  26.                 <Plugin Name="NFLogPlugin" />
  27.             <ConfigPath Name="../" />               
  28.         </MasterServer>
  29.         <ProxyServer>
  30.                 <Plugin Name="NFKernelPlugin" />
  31.                 <Plugin Name="NFConfigPlugin" />
  32.                 <Plugin Name="NFProxyLogicPlugin" />
  33.                 <Plugin Name="NFProxyServerNet_ClientPlugin" />
  34.                 <Plugin Name="NFProxyServerNet_ServerPlugin" />
  35.                 <Plugin Name="NFLogPlugin" />
  36.             <ConfigPath Name="../" />                                
  37.         </ProxyServer>
  38.         <WorldServer>
  39.                 <Plugin Name="NFKernelPlugin" />
  40.                 <Plugin Name="NFConfigPlugin" />
  41.                 <Plugin Name="NFWorldNet_ClientPlugin" />
  42.                 <Plugin Name="NFWorldNet_ServerPlugin" />
  43.                 <Plugin Name="NFLogPlugin" />        
  44.             <ConfigPath Name="../" />               
  45.         </WorldServer>
  46.         
  47.         <TutorialServer>
  48.                 <Plugin Name="NFKernelPlugin" />
  49.                 <Plugin Name="NFConfigPlugin" />
  50.                 <Plugin Name="NFLogPlugin" />        
  51.                 <Plugin Name="Tutorial1" />        
  52.             <ConfigPath Name="../" />               
  53.         </TutorialServer>
  54. </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),然后统一管理所有的插件,并统一进行初始化,反初始化,帧执行等操作,简略代码如下:
 

  1. int main(int argc, char* argv[])
  2. {
  3.         ProcessParameter(argc, argv);
  4.         NFCPluginManager::GetSingletonPtr()->Awake();
  5.         NFCPluginManager::GetSingletonPtr()->Init();
  6.         NFCPluginManager::GetSingletonPtr()->AfterInit();
  7.         NFCPluginManager::GetSingletonPtr()->CheckConfig();
  8.         NFCPluginManager::GetSingletonPtr()->ReadyExecute();
  9.         while (true)
  10.         {
  11.                 std::this_thread::sleep_for(std::chrono::milliseconds(1));
  12.                 NFCPluginManager::GetSingletonPtr()->Execute();
  13.         }
  14.         NFCPluginManager::GetSingletonPtr()->BeforeShut();
  15.         NFCPluginManager::GetSingletonPtr()->Shut();
  16.         NFCPluginManager::GetSingletonPtr()->ReleaseInstance();
  17.         return 0;
  18. }

复制代码


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

  1. class NFIModule
  2. {
  3. public:
  4.         virtual bool Awake()
  5.         {
  6.                 return true;
  7.         }
  8.         virtual bool Init()
  9.         {
  10.                 return true;
  11.         }
  12.         virtual bool AfterInit()
  13.         {
  14.                 return true;
  15.         }
  16.         virtual bool CheckConfig()
  17.         {
  18.                 return true;
  19.         }
  20.         virtual bool BeforeShut()
  21.         {
  22.                 return true;
  23.         }
  24.         virtual bool Shut()
  25.         {
  26.                 return true;
  27.         }
  28.         virtual bool ReadyExecute()
  29.         {
  30.                 return true;
  31.         }
  32.         virtual bool Execute()
  33.         {
  34.                 return true;
  35.         }
  36. };

复制代码


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

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

  1. class NFCDynLib
  2. {
  3. public:
  4.         NFCDynLib(const std::string& strName)
  5.         {
  6.                 mbMain = false;
  7.                 mstrName = strName;
  8. #ifdef NF_DEBUG_MODE
  9.                 mstrName.append("_d");
  10. #endif
  11. #if NF_PLATFORM == NF_PLATFORM_WIN
  12.                 mstrName.append(".dll");
  13. #else
  14.   mstrName.append(".so");
  15. #endif
  16.                 printf("LoadPlugin:%s\n", mstrName.c_str());
  17.         }
  18.         ~NFCDynLib()
  19.         {
  20.         }
  21.         bool Load()
  22.         {
  23.                 std::string strLibPath = "./";
  24.                 strLibPath = mstrName;
  25.                 mInst = (DYNLIB_HANDLE)DYNLIB_LOAD(strLibPath.c_str());
  26.                 return mInst != NULL;
  27.         }
  28.         bool UnLoad()
  29.         {
  30.                 DYNLIB_UNLOAD(mInst);
  31.                 return true;
  32.         }
  33.         const std::string& GetName(void) const
  34.         {
  35.                 return mstrName;
  36.         }
  37.         const bool GetMain(void) const
  38.         {
  39.                 return mbMain;
  40.         }
  41.         void* GetSymbol(const char* szProcName)
  42.         {
  43.                 return (DYNLIB_HANDLE)DYNLIB_GETSYM(mInst, szProcName);
  44.         }
  45. protected:
  46.         std::string mstrName;
  47.         bool mbMain;
  48.         DYNLIB_HANDLE mInst;
  49. };

复制代码


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

  1. bool NFCPluginManager::LoadPluginConfig()
  2. {
  3.     std::string strContent;
  4.     GetFileContent(mstrConfigName, strContent);
  5.     rapidxml::xml_document<> xDoc;
  6.     xDoc.parse<0>((char*)strContent.c_str());
  7.     rapidxml::xml_node<>* pRoot = xDoc.first_node();
  8.     rapidxml::xml_node<>* pAppNameNode = pRoot->first_node(mstrAppName.c_str());
  9.     if (!pAppNameNode)
  10.     {
  11.         NFASSERT(0, "There are no App ID", __FILE__, __FUNCTION__);
  12.         return false;
  13.     }
  14.     for (rapidxml::xml_node<>* pPluginNode = pAppNameNode->first_node("Plugin"); pPluginNode; pPluginNode = pPluginNode->next_sibling("Plugin"))
  15.     {
  16.         const char* strPluginName = pPluginNode->first_attribute("Name")->value();
  17.         mPluginNameMap.insert(PluginNameMap::value_type(strPluginName, true));
  18.     }
  19.     rapidxml::xml_node<>* pPluginConfigPathNode = pAppNameNode->first_node("ConfigPath");
  20.     if (!pPluginConfigPathNode)
  21.     {
  22.         NFASSERT(0, "There are no ConfigPath", __FILE__, __FUNCTION__);
  23.         return false;
  24.     }
  25.     if (NULL == pPluginConfigPathNode->first_attribute("Name"))
  26.     {
  27.         NFASSERT(0, "There are no ConfigPath.Name", __FILE__, __FUNCTION__);
  28.         return false;
  29.     }
  30.     mstrConfigPath = pPluginConfigPathNode->first_attribute("Name")->value();
  31.     return true;
  32. }

复制代码


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

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

  1. virtual bool NFCPluginManager::Init()
  2. {
  3.     PluginInstanceMap::iterator itInstance = mPluginInstanceMap.begin();
  4.     for (itInstance; itInstance != mPluginInstanceMap.end(); itInstance )
  5.     {
  6.         itInstance->second->Init();
  7.     }
  8.     return true;
  9. }

复制代码


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

  1. virtual bool NFIPlugin::Init()
  2.     {
  3.         NFIModule* pModule = First();
  4.         while (pModule)
  5.         {
  6.             bool bRet = pModule->Init();
  7.             if (!bRet)
  8.             {
  9.                 assert(0);
  10.             }
  11.             pModule = Next();
  12.         }

复制代码


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

  1. virtual bool NFCPluginManager::Execute()
  2.     {
  3.         NFIModule* pModule = First();
  4.         while (pModule)
  5.         {
  6.             pModule->Execute();
  7.             pModule = Next();
  8.         }
  9.         return true;
  10.     }
  11.   virtual bool NFIPlugin::Execute()
  12.     {
  13.         NFIModule* pModule = First();
  14.         while (pModule)
  15.         {
  16.             pModule->Execute();
  17.             pModule = Next();
  18.         }
  19.         return true;
  20.     }

复制代码


在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库,以及数据驱动等新技术,能大幅提升开发效率节省开发周期以及提高程序的稳定性。

 类似资料: