共享内存的设计
一、服务器构架
一个天龙八部游戏区,主要服务器部署情况如下图所示:
实际部署可能有所不同。区角色数据库可以安装到Machine4,那么一个区有5台物理机器。LoginServer和WorldServer、CharacterDB、BillingServer有连接。WorldServer和各个GameServer有连接。ShareMemory和CharacterDB有连接。
一台物理机器上,会启动一个ShareMemory进程和一个服务器进程,服务器进程有世界服务器和游戏服务器。天龙八部的世界是ZoneBase的,一个游戏服务器服务启动多个线程,每个线程服务若干个场景。
在ShareMemory进程、WorldServer和GameServer进程,都需要对多种共享内存池进行初始化,初始化传入的类型是不同的,类型定义如下:
enum SMPOOL_TYPE
{
SMPT_SHAREMEM,
SMPT_SERVER,
SMPT_WORLD
};
共享内存池对象在ShareMemory进程中初始化时候,才会真正分配共享内存对象,并把池对象Attach到共享内存对象。在WorldServer和GameServer进程中,池对象只是获取共享内存对象并Attach。
天龙八部使用的共享内存对象有:
角色 SMUPool<HumanSMU> 类型ST_HUMAN_SMU;
公会 SMUPool<GuildSMU> 类型ST_GUILD_SMU;
邮件 SMUPool<MailSMU> 类型ST_MAIL_SMU;
玩家商店 SMUPool<PlayerShopSM> 类型ST_PSHOP_SMU;
道具序列号SMUPool<ItemSerialKeySMU> 类型ST_ITEMSERIAL_SMU;
二、共享内存模块
几个关键的类和结构定义如下图:
ShareMemAO封装了系统共享内存API功能,提供了池对象Attach/Detach接口以及转储文件功能。
SMUPool模板类是具体的池对象,天龙八部实现的池类型有5种,具体数据类型对应5个结构:HumanSMU、GuildSMU、MailSMU、PlayerShopSM和ItemSerialKeySMU。
SMULogicManager模板类提供了对这些共享内存池对象进行管理的功能,如初始化、心跳、保存数据库、清理等操作。
三、ShareMemory进程中共享内存模块的主要功能
具体见ShareMemory.h/ShareMemory.cpp,该服务进程主要功能是:
l 根据配置创建共享内存池对象(SMUPool)和池管理对象(SMULogicManager);
l 对创建的内存池对象和池管理对象进行初始化,注意初始化传入的类型是SMPT_SHAREMEM;如果是这个类型,底层会分配具体的共享内存,而不是仅仅的Attach;
l 对共享内存池管理对象进行更新(心跳),会进行数据库存储等操作,这里是保存数据库的唯一地方。那么数据从数据库的加载是在哪里?下面会介绍,是在LonginServer进程里面。
关于数据库模块,顺带提一下,从代码看天龙居然用的还是ODBC接口,后来应该改了吧。
四、WorldServer中的共享内存池对象
具体见ShareMemManager.h/ShareMemManager.cpp。WorldServer中仅对两类池对象进行操作,他们是GuildSMU和MailSMU。
extern SMUPool<GuildSMU> g_GuildSMUPool;
extern SMUManager<GuildSMU> g_GuildSMUManager;
extern SMUPool<MailSMU> g_MailSMUPool;
extern SMUManager<MailSMU> g_MailSMUManager;
两个共享内存池对象的初始化在World.cpp的BOOL World::NewStaticManager( )函数中,注意此处传入的类型是SMPT_WORLD。
五、GameServer中的共享内存池对象
具体见ShareMemManager.h/ShareMemManager.cpp。GameServer中对三类池对象进行操作,他们是HumanSMU、PlayerShopSM和ItemSerialKeySMU。
SMUPool<HumanSMU> g_HumanSMUPool;
SMUPool<PlayerShopSM> g_PlayerShopSMUPool;
SMUPool<ItemSerialKeySMU> g_ItemSerialSMUPool;
这些共享内存池对象的初始化在Server.cpp的BOOL Server::NewStaticManager( )函数中,注意此处传入的类型是SMPT_SERVER。
六、样例分析——玩家角色数据的存储和共享
服务器端玩家角色对于的类型是class Obj_Human,里面有角色数据库存储接口的定义:
protected:
//存放所有关于Obj_Human的、从数据库里读取的信息
HumanDB m_DB;
class HumanDB //中拥有内存共享对象指针,以及角色需要存储的数据定义。
private:
//共享内存相关数据
HumanSMU* m_HumanSMU; //共享内存数据
HUMAN_DB_ATTR_FLAG* m_AttrFlag; //角色属性刷新控制数据
private:
_HUMAN_DB_LOAD* m_dbHuman ; //角色基本信息
_BAG_DB_LOAD* m_dbBag ; //角色背包物品信息
_EQUIP_DB_LOAD* m_dbEquip ; //角色装备信息
_BANK_DB_LOAD* m_dbBank ; //角色银行物品信息
_SKILL_DB_LOAD* m_dbSkill ; //角色身上拥有的技能信息
_COOLDOWN_DB_LOAD_FOR_HUMAN* m_dbCooldown ; //角色身上的冷却信息
_RELATION_DB_LOAD* m_dbRelation; //角色联系人(好友、黑名单)
_ABILITY_DB_LOAD* m_dbAbility; //角色学会的生活技能信息以及配方表
_XINFA_DB_LOAD* m_dbXinFa ; //角色学会的心法信息
_IMPACT_DB_LOAD* m_dbImpact ; //角色身上所施加的附加效果信息
_MISSION_DB_LOAD* m_dbMission; //任务列表
_SETTING_DB_LOAD* m_dbSetting ; //设置数据
_PRIVATE_INFO_DB_LOAD* m_dbPrivateInfo;//私人信息
上面这些xxx_LOAD很眼熟,其实在struct HumanSMU里面有类似定义:
struct HumanSMU
{
SMUHead m_SMUHead;
HUMAN_DB_ATTR_FLAG m_AttrFlag; //角色属性标志位
_HUMAN_DB_LOAD m_HumanSM ; //角色基本信息
_BANK_DB_LOAD m_BankSM ; //角色银行物品信息
_SKILL_DB_LOAD m_SkillSM ; //角色身上拥有的技能信息
_COOLDOWN_DB_LOAD_FOR_HUMAN m_CooldownSM ; //角色身上的冷却信息
_XINFA_DB_LOAD m_XinFaSM ; //角色学会的心法信息
_IMPACT_DB_LOAD m_ImpactSM ; //角色身上所施加的附加效果信息
_ABILITY_DB_LOAD m_AbilitySM; //角色学会的生活技能信息以及配方表
_MISSION_DB_LOAD m_MissionSM; //任务列表
_SETTING_DB_LOAD m_SettingSM; //任务列表
_PET_DB_LIST_LOAD m_PetListSM; //宠物列表
_BAG_DB_LOAD m_BagSM; //角色背包物品信息
_EQUIP_DB_LOAD m_EquipSM; //角色装备信息
_RELATION_DB_LOAD m_RelationSM; //角色联系人(好友、黑名单)
_PRIVATE_INFO_DB_LOAD m_PrivateInfoSM;//私人信息
};
再看看HumanDB如何初始化的,看其构造函数(代码太多,删除了一些):
HumanDB::HumanDB( )
{
//这里获取共享内存存储单元
HumanSMU* pSMU = g_HumanSMUPool.NewObj();
m_HumanSMU = pSMU;
m_AttrRegSM = new HUMAN_DB_ATTR_REG;
m_AttrReg = new HUMAN_DB_ATTR_REG;
m_AttrFlag = new HUMAN_DB_ATTR_FLAG;
m_dbHuman = new _HUMAN_DB_LOAD ;
//...略过一些对象内存分配
//属性表和DB数据的挂接,用于脏数据判断和数据位置对接
_RegisterDBAttributes();
//属性表和内存共享对象挂接,用于脏数据判断和数据位置对接
_RegisterSMAttributes();
}
1. Human数据的加载是通过DBCharFullData进行的,检查了一下代码,加载的地方只有两个地方,一个是创建角色的消息响应,另外一个地方是角色登录消息响应,这些都是LoginServer(登录服务器)处理的。
Server\Login\Packets\CLAskCreateCharHander.cpp
Server\Login\Packets\WLRetCharLoginHandler.cpp
角色数据被加载后,发给了WorldServer,又通过WorldServer发送给GameServer。
2. GameServer的消息响应如下:
UINT WGRetUserDataHandler::Execute( WGRetUserData* pPacket, Player* pPlayer )
//这么大的数据包是通过网络发过来的
pGamePlayer->InitHuman( pPacket->GetUserData(),UDR_USERDATA, pPacket->GetPlayerAge() ) ;
//InitHuman里面对HumanDB数据进行赋值
_OBJ_HUMAN_INIT initHuman;
initHuman.m_pUseData = pData;
m_pHuman->Init( &initHuman, age ) ;
m_pHuman->ValidateShareMem(TRUE); //再把数据复制到内存共享单元里面
3. HumanDB通过ValidateShareMem函数把数据复制到共享内存单元。
VOID HumanDB::ValidateShareMem(BOOL bForceAll,BOOL& bUpdateAttr)
4. 在Obj_Human的更新函数里面把更新后的数据复制到共享内存单元。
BOOL Obj_Human::HeartBeat( UINT uTime )
ValidateShareMem(FALSE); //最后更新数据到共享内存
七、总结
1. TL为什么要用共享内存
因为天龙八部的世界是ZoneBase的,一个区有多台游戏服务器,一台游戏服务器服务多个场景,一台游戏服务器上有多个场景线程。玩家会频繁穿越场景,穿越场景时,玩家角色数据可能需要跨线程、跨进程和物理机器。使用共享内存使玩家在跨物理机器时候,数据也不用保存到数据库并且再次从数据库加载,提高了效率(需要通过网络发送数据,但是省下了一次写数据库和一次读数据库操作)。
可以集中保存数据到数据库、集中控制保存的频率、也保证了世界的数据一致性。
2. 共享内存并没节省游戏服务器内存
可以看到,角色数据除了在共享内存中有一份外,HumanDB也有一份。HumanDB在Obj_Human::HeartBeat中把游戏更新的数据保存到共享内存。
以前有个想法,认为既然内存数据被共享了,那么各个游戏服务器(场景)里面不需要再有一份数据,可以节省一些内存。因为共享内存也需要定时存盘,如果只有一份数据,游戏服务器和共享内存进程的互斥会降低游戏服务器效率。