请选择 进入手机版 | 继续访问电脑版

NoahFrame

 找回密码
 Register Now
搜索
热搜: redis mysql tutorial
查看: 2654|回复: 0

第六章 NF分布式服务器解决方案--自动化的数据映射

[复制链接]

30

主题

111

帖子

650

积分

Administrator

Rank: 9Rank: 9Rank: 9

积分
650
发表于 2016-12-30 10:00:35 | 显示全部楼层 |阅读模式
NF(https://github.com/ketoo/NoahGameFrame)全称为 NoahFrame/NoahGameFrame。


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


关键词


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





我们之前有说过,NF面向数据编程,所有的数据经设计直接拥有,全是自动化过程,数据驱动的时候,每次变更都会自动回调。那么,如果我们把这个技术使用在对客户端的广播上,是不是很爽?因为统一的属性管理机制,所有的广播代码写一份即可支持所有的广播,同时因为不会产生广播信息冗余,而极大的节约了带宽和流量资源。

那我们现在就先来理一理,数据广播的原理。


数据广播有一个很重要的原则就是:首次广播全部需要广播的有效数据,后续只广播变化且需要广播的有效数据(这些可以在属性系统中,增加对属性的描述,描述此属性是否需要广播)。


NF中,需要广播给客户端的数据大体分围一下3类数据:

1:Property
2:Record
3:复杂的Element(其实复杂的Element也就是嵌套或者多条服务器数据,每次客户端登录成功后发送到客户端,在NF中本身支持嵌套string中会自动按照,;拆分)

第三类数据,与具体的业务逻辑关系太大,但是可以基于第一类数据的广播而广播(本身就是string而已)。下面就说一下如何自动化的广播Property(Record广播和Property简直一模一样),我们拿player类的部分数据来做示例(Player.xml),比如HP Level等,内容如下所示:

  1. <font size="3"><XML>
  2.     <Propertys>
  3.         <Property Id="Name"    Type="string"     Public="1" Private="1" Save="1" Desc="角色名" />
  4.         <Property Id="Level"     Type="int"         Public="1" Private="1" Save="1" Desc="等级"  />
  5.         <Property Id="EXP"       Type="int"         Public="1" Private="1" Save="1" Desc="经验"  />
  6.         <Property Id="HP"        Type="int"         Public="1" Private="1" Save="1" Desc="生命"  />
  7.         <Property Id="SP"        Type="int"         Public="1" Private="1" Save="1" Desc="体力"  />
  8.     </Propertys></font>
复制代码




Type: 表示字段类型(比如Name,Level的类型),比如string, int, float, object, vector2等几种类型(在NF中,其中int默认为64位,float实际是double实现)
Public : 表示此字段是否公开给其他人(广播给同场景的其他player,包含自己)
Private: 表示是否下发给客户端自己(广播给玩家自己,public起作用了,则这个不起作用)
Save: 表示是否存入数据库,是否存档
Desc: 自己看了,自己备注,无所谓的内容

我们在 <<第四章 NF分布式服务器解决方案–通用的数据驱动模型>>中有说到数据驱动的思想,当数据变化的时候会产生回调(委托)事件,以便驱动业务逻辑运行。同理,我们广播数据,也是在此上面注册观察者,观察者观察到数据改变,则通过通用的消息协议下发到客户端,保证数据实效性。那么,首先,是不是就要对想广播给客户端的所有数据添加观察者呢:在模块启动的时候(Init 或者 AfterInit函数),调用RegisterCommonPropertyEvent函数 注册观测者,然后任何NFClass的任何属性变动,都会回调到OnPropertyCommonEvent函数中,代码如下:
  1. <font size="3">
  2. bool NFCGameServerNet_ServerModule::AfterInit()
  3. {
  4.     m_pKernelModule->RegisterCommonPropertyEvent(this, &NFCGameServerNet_ServerModule::OnPropertyCommonEvent);
  5.    
  6.     return true;
  7. }

  8. int NFCGameServerNet_ServerModule::OnPropertyCommonEvent(const NFGUID& self, const std::string& strPropertyName, const NFIDataList::TData& oldVar, const NFIDataList::TData& newVar)
  9. {
  10.     //根据属性名,和属性的 public 和 private 属性,得知需要广播给哪些对象(这些对象都和self对象在同一个场景副本)
  11.     NFCDataList valueBroadCaseList;
  12.     int nCount = GetBroadCastObject(self, strPropertyName, valueBroadCaseList);

  13.     if (valueBroadCaseList.GetCount() <= 0)
  14.     {
  15.         //无需广播
  16.         return 0;
  17.     }

  18.     switch (oldVar.GetType())
  19.     {
  20.         case TDATA_INT:
  21.         {
  22.             //所有的int类型的广播,都用此协议: property_name -> data
  23.             NFMsg::ObjectPropertyInt xPropertyInt;
  24.             NFMsg::Ident* pIdent = xPropertyInt.mutable_player_id();
  25.             *pIdent = NFToPB(self);

  26.             NFMsg::PropertyInt* pDataInt = xPropertyInt.add_property_list();
  27.             pDataInt->set_property_name(strPropertyName);
  28.             pDataInt->set_data(newVar.GetInt());

  29.             for (int i = 0; i < valueBroadCaseList.GetCount(); i++)
  30.             {
  31.                 NFGUID identOld = valueBroadCaseList.Object(i);
  32.                 //发送到网关
  33.                 SendMsgPBToGate(NFMsg::EGMI_ACK_PROPERTY_INT, xPropertyInt, identOld);
  34.             }
  35.         }
  36.         break;

  37.         case TDATA_FLOAT:
  38.         {
  39.             //所有的float类型的广播,都用此协议: property_name -> data
  40.             NFMsg::ObjectPropertyFloat xPropertyFloat;
  41.             NFMsg::Ident* pIdent = xPropertyFloat.mutable_player_id();
  42.             *pIdent = NFToPB(self);

  43.             NFMsg::PropertyFloat* pDataFloat = xPropertyFloat.add_property_list();
  44.             pDataFloat->set_property_name(strPropertyName);
  45.             pDataFloat->set_data(newVar.GetFloat());

  46.             for (int i = 0; i < valueBroadCaseList.GetCount(); i++)
  47.             {
  48.                 NFGUID identOld = valueBroadCaseList.Object(i);
  49.                 //发送到网关
  50.                 SendMsgPBToGate(NFMsg::EGMI_ACK_PROPERTY_DOUBLE, xPropertyFloat, identOld);
  51.             }
  52.         }
  53.         break;

  54.         case TDATA_STRING:
  55.         {
  56.             //所有的string类型的广播,都用此协议: property_name -> data--player类中的Name属性就会走这里
  57.             NFMsg::ObjectPropertyString xPropertyString;
  58.             NFMsg::Ident* pIdent = xPropertyString.mutable_player_id();
  59.             *pIdent = NFToPB(self);

  60.             NFMsg::PropertyString* pDataString = xPropertyString.add_property_list();
  61.             pDataString->set_property_name(strPropertyName);
  62.             pDataString->set_data(newVar.GetString());

  63.             for (int i = 0; i < valueBroadCaseList.GetCount(); i++)
  64.             {
  65.                 NFGUID identOld = valueBroadCaseList.Object(i);
  66.                 //发送到网关
  67.                 SendMsgPBToGate(NFMsg::EGMI_ACK_PROPERTY_STRING, xPropertyString, identOld);
  68.             }
  69.         }
  70.         break;

  71.         case TDATA_OBJECT:
  72.         {
  73.             //所有的object类型的广播,都用此协议: property_name -> data
  74.             NFMsg::ObjectPropertyObject xPropertyObject;
  75.             NFMsg::Ident* pIdent = xPropertyObject.mutable_player_id();
  76.             *pIdent = NFToPB(self);

  77.             NFMsg::PropertyObject* pDataObject = xPropertyObject.add_property_list();
  78.             pDataObject->set_property_name(strPropertyName);
  79.             *pDataObject->mutable_data() = NFToPB(newVar.GetObject());

  80.             for (int i = 0; i < valueBroadCaseList.GetCount(); i++)
  81.             {
  82.                 NFGUID identOld = valueBroadCaseList.Object(i);
  83.                 //发送到网关
  84.                 SendMsgPBToGate(NFMsg::EGMI_ACK_PROPERTY_OBJECT, xPropertyObject, identOld);
  85.             }
  86.         }
  87.         break;

  88.         default:
  89.             break;
  90.     }


  91.     return 0;
  92. }
  93. </font>
复制代码


那么肯定有同学对 GetBroadCastObject 函数有较大疑问了,GetBroadCastObject主要目的是根据 对象(self) + 属性(strPropertyName) 计算出应该广播给哪些其他对象,返回一个对象列表,函数如下:


  1. <font size="3">
  2. int NFCGameServerNet_ServerModule::GetBroadCastObject(const NFGUID& self, const std::string& strPropertyName, NFIDataList& valueObject)
  3. {
  4.     //得到self对象所在的SceneID 和 GroupID
  5.     int nObjectSceneID = m_pKernelModule->GetPropertyInt(self, NFrame::IObject::SceneID());
  6.     int nObjectGroupID = m_pKernelModule->GetPropertyInt(self, NFrame::IObject::GroupID());

  7.     //得到self对象的逻辑类名
  8.     const std::string& strClassName = m_pKernelModule->GetPropertyString(self, NFrame::IObject::ClassName());
  9.    
  10.     //得到self对象的属性管理器
  11.     NF_SHARE_PTR<NFIPropertyManager> pClassPropertyManager = m_pLogicClassModule->GetClassPropertyManager(strClassName);
  12.     if (NULL == pClassPropertyManager)
  13.    {
  14.        return -1;
  15.    }
  16.    
  17.    //得到self对象的strPropertyName属性对象
  18.    NF_SHARE_PTR<NFIProperty> pProperty = pClassPropertyManager->GetElement(strPropertyName);
  19.    if (NULL == pProperty.get())
  20.    {
  21.        return -1;
  22.    }
  23.    
  24.    //如果此属性,是要广播给其他人的,则根据场景ID 和 GroupID得到他周围的所有可广播的对象(包含自己在内)
  25.    if (pProperty->GetPublic())
  26.    {
  27.        GetBroadCastObject(nObjectContainerID, nObjectGroupID, valueObject);
  28.    }
  29.    //否则,如果只需要广播给自己,则广播列表只加入自己即可
  30.    else if (pProperty->GetPrivate())
  31.    {
  32.        valueObject.Add(self);
  33.    }

  34.    return valueObject.GetCount();
  35. }
  36. </font>
复制代码

从上述代码可以看出,NF中,依赖数据驱动,一个通用的增量广播就轻松实现了,但是有同学肯定会问,你这是在游戏过程中的玩家数据变化啊,那么玩家刚上线那一下呢,数据那么多怎么整?其实玩家刚上线,我认为更好整,遍历所有的属性,加入广播列表(使用PB结构体PlayerPropertyBase),几十行代码即可解决!
在上面我们可以看出,所有的数据被抽象成为4类数据(int,float,string,object),在广播的时候也是根据这4类分类的,我们来看看这4个数据消息体的结构(protocol buff, 目录为: NFComm\NFMessageDefine\NFMsgBase.proto):
  1. <font size="3">
  2. ObjectPropertyInt(内部包含可重叠的PropertyInt)
  3. ObjectPropertyFloat(内部包含可重叠的PropertyFloat)
  4. ObjectPropertyString(内部包含可重叠的PropertyString)
  5. ObjectPropertyObject(内部包含可重叠的PropertyObject)


  6. message Ident//基础结构,不直接发送
  7. {
  8.     required int64 svrid = 1;
  9.     required int64 index = 2;
  10. }
  11. ////////////////////////BaseCommon/////////////////////////////
  12. message PropertyInt//基础结构,不直接发送
  13. {
  14.     required bytes property_name = 1;
  15.     required int64 data = 2;
  16. }

  17. message PropertyFloat//基础结构,不直接发送
  18. {
  19.     required bytes property_name = 1;
  20.     required float data = 2;
  21. }

  22. message PropertyString//基础结构,不直接发送
  23. {
  24.     required bytes property_name = 1;
  25.     required bytes data = 2;
  26. }

  27. message PropertyObject//基础结构,不直接发送
  28. {
  29.     required bytes property_name = 1;
  30.     required Ident data = 2;
  31. }

  32. message ObjectPropertyInt//个人玩家单类型属性数据,可直接发送---变化时
  33. {
  34.      required Ident player_id = 1;
  35.      repeated PropertyInt property_list = 2;
  36. }

  37. message ObjectPropertyFloat//个人玩家单类型属性数据,可直接发送---变化时
  38. {
  39.      required Ident player_id = 1;
  40.      repeated PropertyFloat property_list = 2;
  41. }

  42. message ObjectPropertyString//个人玩家单类型属性数据,可直接发送---变化时
  43. {
  44.      required Ident player_id = 1;
  45.      repeated PropertyString property_list = 2;
  46. }

  47. message ObjectPropertyObject//个人玩家单类型属性数据,可直接发送---变化时
  48. {
  49.      required Ident player_id = 1;
  50.      repeated PropertyObject property_list = 2;
  51. }

  52. message PlayerPropertyBase//个人数据,可直发身上所有的属性数据
  53. {
  54.      repeated PropertyInt property_int_list = 1;
  55.      repeated PropertyFloat property_float_list = 2;
  56.      repeated PropertyString property_string_list = 3;
  57.      repeated PropertyObject property_object_list = 4;
  58. }
  59. </font>
复制代码


此类消息体,占据了整个游戏项目的80%以上的消息类型,因为我们把所有的Property数据变化都通过这4类消息传输到客户端(无论是传输给客户端自己,还是广播给其他人)。如果加上record类消息,则可以概括一个小游戏的95%的数据传输协议(因为在NF中,所有的数据,均存在于Propert, Record系统中,不会有任何其他系统用于存储用户数据),对于后续维护来说,效率的非常高的,这也是为什么抽象出这几类数据出来的原因之一。

看了Property的广播原理后,Record的原理就可以不用说了,除了有AddRow,RemoveRow外,其他的都和Property一样。有了抽象的Property,Record这2大利器,游戏开发中,可以节省50%以上的开发和设计时间!妈妈再也不用担心我会加班了!

NF项目为开源的分布式服务器解决方案,其中包含了网络库,actor库,以及数据驱动等新技术,能大幅提升开发效率节省开发周期以及提高程序的稳定性。
项目地址 https://github.com/ketoo/NoahGameFrame
如感觉对您有帮助,请给与star,同时也邀请广大同行参与开发和维护,作者QQ 342006,交流QQ群 341159815。
欢迎转载,转载请注明来源,本文版权归作者所有!




回复

使用道具 举报

您需要登录后才可以回帖 登录 | Register Now

本版积分规则

 

GMT+8, 2018-10-19 02:05 , Processed in 0.072115 second(s), 25 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表