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.  
  12. <LoginServer>
  13. <Plugin Name=NFKernelPlugin />
  14. <Plugin Name=NFConfigPlugin />
  15. <Plugin Name=NFLoginLogicPlugin />
  16. <Plugin Name=NFLoginNet_ServerPlugin />
  17. <Plugin Name=NFLoginNet_ClientPlugin />
  18. <Plugin Name=NFLogPlugin />
  19. <ConfigPath Name=../ />
  20. </LoginServer>
  21.  
  22. <MasterServer>
  23. <Plugin Name=NFKernelPlugin />
  24. <Plugin Name=NFConfigPlugin />
  25. <Plugin Name=NFMasterServerPlugin />
  26. <Plugin Name=NFMasterNet_ServerPlugin />
  27. <Plugin Name=NFMasterNet_HttpServerPlugin />
  28. <Plugin Name=NFLogPlugin />
  29. <ConfigPath Name=../ />
  30. </MasterServer>
  31.  
  32. <ProxyServer>
  33. <Plugin Name=NFKernelPlugin />
  34. <Plugin Name=NFConfigPlugin />
  35. <Plugin Name=NFProxyLogicPlugin />
  36. <Plugin Name=NFProxyServerNet_ClientPlugin />
  37. <Plugin Name=NFProxyServerNet_ServerPlugin />
  38. <Plugin Name=NFLogPlugin />
  39. <ConfigPath Name=../ />
  40. </ProxyServer>
  41.  
  42. <WorldServer>
  43. <Plugin Name=NFKernelPlugin />
  44. <Plugin Name=NFConfigPlugin />
  45. <Plugin Name=NFWorldNet_ClientPlugin />
  46. <Plugin Name=NFWorldNet_ServerPlugin />
  47. <Plugin Name=NFLogPlugin />
  48. <ConfigPath Name=../ />
  49. </WorldServer>
  50.  
  51. <TutorialServer>
  52. <Plugin Name=NFKernelPlugin />
  53. <Plugin Name=NFConfigPlugin />
  54. <Plugin Name=NFLogPlugin />
  55. <Plugin Name=Tutorial1 />
  56. <ConfigPath Name=../ />
  57. </TutorialServer>
  58. </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.  
  5. NFCPluginManager::GetSingletonPtr()->Awake();
  6. NFCPluginManager::GetSingletonPtr()->Init();
  7. NFCPluginManager::GetSingletonPtr()->AfterInit();
  8. NFCPluginManager::GetSingletonPtr()->CheckConfig();
  9. NFCPluginManager::GetSingletonPtr()->ReadyExecute();
  10.  
  11. while (true)
  12. {
  13. std::this_thread::sleep_for(std::chrono::milliseconds(1));
  14.  
  15. NFCPluginManager::GetSingletonPtr()->Execute();
  16. }
  17.  
  18. NFCPluginManager::GetSingletonPtr()->BeforeShut();
  19. NFCPluginManager::GetSingletonPtr()->Shut();
  20.  
  21. NFCPluginManager::GetSingletonPtr()->ReleaseInstance();
  22.  
  23. return 0;
  24. }
复制代码
因为任何一个插件(Plugin)必须继承自NFIPlugin类,它拥有NFIModule类所有的统一调用接口(任何模块,必须继承自NFIModule并拥有以下接口Awake, Init, AfterInit, Execute, BeforeShut, Shut,Finalize等统一接口):

 

 

  1. class NFIModule
  2. {
  3.  
  4. public:
  5. virtual bool Awake()
  6. {
  7. return true;
  8. }
  9.  
  10. virtual bool Init()
  11. {
  12. return true;
  13. }
  14.  
  15. virtual bool AfterInit()
  16. {
  17. return true;
  18. }
  19.  
  20. virtual bool CheckConfig()
  21. {
  22. return true;
  23. }
  24.  
  25. virtual bool BeforeShut()
  26. {
  27. return true;
  28. }
  29.  
  30. virtual bool Shut()
  31. {
  32. return true;
  33. }
  34.  
  35. virtual bool ReadyExecute()
  36. {
  37. return true;
  38. }
  39.  
  40. virtual bool Execute()
  41. {
  42. return true;
  43. }
  44. };
复制代码
这些函数被调用的顺序,和NFCPluginManager启动的时候调用的顺序是一样的,每一个继承NFIModule的类(Mudole),都是按照此顺序运行。

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

 

 

  1. class NFCDynLib
  2. {
  3.  
  4. public:
  5.  
  6. NFCDynLib(const std::string strName)
  7. {
  8. mbMain = false;
  9. mstrName = strName;
  10. #ifdef NF_DEBUG_MODE
  11. mstrName.append(_d);
  12. #endif
  13.  
  14. #if NF_PLATFORM == NF_PLATFORM_WIN
  15. mstrName.append(.dll);
  16. #else
  17. mstrName.append(.so);
  18. #endif
  19.  
  20. printf(LoadPlugin:%s\n, mstrName.c_str());
  21. }
  22.  
  23. ~NFCDynLib()
  24. {
  25.  
  26. }
  27.  
  28. bool Load()
  29. {
  30. std::string strLibPath = ./;
  31. strLibPath = mstrName;
  32. mInst = (DYNLIB_HANDLE)DYNLIB_LOAD(strLibPath.c_str());
  33.  
  34. return mInst != NULL;
  35. }
  36.  
  37. bool UnLoad()
  38. {
  39. DYNLIB_UNLOAD(mInst);
  40. return true;
  41. }
  42.  
  43. const std::string GetName(void) const
  44. {
  45. return mstrName;
  46. }
  47.  
  48. const bool GetMain(void) const
  49. {
  50. return mbMain;
  51. }
  52.  
  53. void* GetSymbol(const char* szProcName)
  54. {
  55. return (DYNLIB_HANDLE)DYNLIB_GETSYM(mInst, szProcName);
  56. }
  57.  
  58. protected:
  59.  
  60. std::string mstrName;
  61. bool mbMain;
  62.  
  63. DYNLIB_HANDLE mInst;
  64. };
复制代码
然后NFCPluginManager::Awake()函数在调用的时候,会先调用LoadPluginConfig()函数来动态库通过查找Plugin.xml内部配置的插件列表来加载需要的插件:

 

 

  1. bool NFCPluginManager::LoadPluginConfig()
  2. {
  3. std::string strContent;
  4. GetFileContent(mstrConfigName, strContent);
  5.  
  6. rapidxml::xml_document<> xDoc;
  7. xDoc.parse<0>((char*)strContent.c_str());
  8.  
  9. rapidxml::xml_node<>* pRoot = xDoc.first_node();
  10. rapidxml::xml_node<>* pAppNameNode = pRoot->first_node(mstrAppName.c_str());
  11. if (!pAppNameNode)
  12. {
  13. NFASSERT(0, There are no App ID, __FILE__, __FUNCTION__);
  14. return false;
  15. }
  16.  
  17. for (rapidxml::xml_node<>* pPluginNode = pAppNameNode->first_node(Plugin); pPluginNode; pPluginNode = pPluginNode->next_sibling(Plugin))
  18. {
  19. const char* strPluginName = pPluginNode->first_attribute(Name)->value();
  20.  
  21. mPluginNameMap.insert(PluginNameMap::value_type(strPluginName, true));
  22.  
  23. }
  24.  
  25. rapidxml::xml_node<>* pPluginConfigPathNode = pAppNameNode->first_node(ConfigPath);
  26. if (!pPluginConfigPathNode)
  27. {
  28. NFASSERT(0, There are no ConfigPath, __FILE__, __FUNCTION__);
  29. return false;
  30. }
  31.  
  32. if (NULL == pPluginConfigPathNode->first_attribute(Name))
  33. {
  34. NFASSERT(0, There are no ConfigPath.Name, __FILE__, __FUNCTION__);
  35. return false;
  36. }
  37.  
  38. mstrConfigPath = pPluginConfigPathNode->first_attribute(Name)->value();
  39.  
  40. return true;
  41. }
复制代码
其次会开始执行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.  
  9. return true;
  10. }
复制代码
//而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.  
  12. pModule = Next();
  13. }
复制代码
当所有的初始化成功之后,接着就会开始帧循环,以便网络库,心跳库等能够有机会处理自己的逻辑(比如接受消息,然后调用各逻辑模块驱动逻辑进行;又比如到点发奖等内容)代码如下,是不是和Init几乎一样?:

 

 

  1. virtual bool NFCPluginManager::Execute()
  2. {
  3. NFIModule* pModule = First();
  4. while (pModule)
  5. {
  6. pModule->Execute();
  7.  
  8. pModule = Next();
  9. }
  10.  
  11. return true;
  12. }
  13.  
  14. virtual bool NFIPlugin::Execute()
  15. {
  16. NFIModule* pModule = First();
  17. while (pModule)
  18. {
  19. pModule->Execute();
  20.  
  21. pModule = Next();
  22. }
  23.  
  24. return true;
  25. }
复制代码
在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库,以及数据驱动等新技术,能大幅提升开发效率节省开发周期以及提高程序的稳定性。

相关阅读:开源服务器框架NF分享(一):游戏服务器的进化

via:GAD

声明:游资网登载此文出于传递信息之目的,绝不意味着游资网赞同其观点或证实其描述。
锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛