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加载),例如:
- <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)必须继承QQ拍卖平台自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\n", 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库,以及数据驱动等新技术,能大幅提升开发效率节省开发周期以及提高程序的稳定性。