分布式游戏服务器框架sframe(五)—— 配置管理

来源:互联网 发布:域名免费备案 编辑:程序博客网 时间:2024/05/16 19:17

        开发游戏服务器,肯定会有大量的配置数据,什么等级配置、关卡配置等等。对于这些静态数据,我们一般都是采取文件的方式来存储的。策划事先配置好这些数据文件,服务器启动后便将其加载到内存,由配置管理模块统一管理。对于配置文件,有各种各样的格式,CSV、JSON、XML、LUA等等都很流行。但是对于程序来说,我们希望无论配置文件采用什么格式,数据加载到内存后都是一样的存在形式。

         sframe提供了一套配置管理机制来实现配置文件的加载、配置数据管理的功能。它负责解析配置文件,并将配置数据映射到c++的对象。对于开发者,我们使用的数据都在这些对象中,而不用关心配置文件到底是什么格式的。目前只支持json、csv两种格式的文件的解析和加载。

         sframe的配置管理相关的代码都在sframe/sfram/conf目录中。本篇文章着重讲解如何使用sframe的配置管理,至于它的实现,就是一系列c++模板使用技巧,至于其原理,和前面讲的序列化、消息映射都是差不多的。只要理解了前面所讲内容,再看配置管理的源码,应该就比较好理解了。所以这里就不讲实现了,感兴趣的同学看源代码足矣。

加载配置文件

         sframe可解析json、csv格式的文件,然后将数据映射到c++对象。sframe加载配置数据的步骤如下:

        ◆ 读取配置文件。

        ◆解析配置文件。读取了文件后,要解析文件的格式。csv格式很简单,所以我自己写了解析算法,将文件内容,解析为一个sframe::Table对象。json文件的解析,我采用了github上的json11(https://github.com/dropbox/json11)。这个库很精简,只有一个头文件加一个源文件,而且对外接口也是我喜欢的风格,所以就直接用了。它解析文件内容后,输出一个json11::Json对象。

        ◆ 将数据映射到对象。解析了之后,就要将数据映射到对象。之后我们直接接触的就是这个对象了,再也与具体的文件无关了。

        上面3步就完成了配置数据的加载。第1、2步都很好理解,这里就不多说了。接下来主要讲讲第3步——将数据映射到对象。sframe提供了比较方便的接口来完成映射功能。json和csv的接口以及需要引用的头文件都不一样,下面我分开来说明。

1.       json数据映射到c++对象

        需要引用头文件"conf/JsonReader.h"。然后调用sframe::ConfigLoader::Load<constjson11::Json>(json, obj),即可完成将json11::Json对象的数据映射到obj对象。obj可以是所有c++基础类型、std::string、std::unordered_map<T_Key, T_Val>、std::map<T_Key,T_Val>、std::vector<T>、std::list<T>、std::set<T>、std::unordered_set<T>、std::shared_ptr<T>、以及以上所有类型的数组。json自己就有几种数据类型,这些数据类型与c++的类型对应关系见下表:

json数据类型

 

C++数据类型

Bool、Number、String

 

bool、任意数值类型、std::string

ARRAY

 

数组、std::map、std::unordered_map、std::vector、std::list、std::set、std::unordered_set

OBJECT

 

std::map、std::unordered_map、自定义结构体

        这里要补充说明的是,JSON的OBJECT映射到c++的自定义结构体对象,还需要给结构体定义一个Fill()成员函数,以完成填充。具体的后面讲解。

        现比如说有JSON数据{“name”:”小明”,“address”:””},解析后我们得到一个json11::Json的对象json,欲将其映射到std::map<std::string, std::string> obj。直接调用sframe::ConfigLoader::Load<constjson11::Json>(json, obj)即可。

2.       csv数据映射到c++对象

        需要引用头文件"conf/TableReader.h"。调用的方法是sframe::ConfigLoader::Load<TableReader>(tbl,obj)。

        不同的是,csv是典型的表格形式的格式。对于整张表,可以映射到std::unordered_map<T_Key, T_Val>、std::map<T_Key,T_Val>、std::vector<T>、std::list<T>、std::set<T>、std::unordered_set<T>、以及以上所有类型的std::shared_ptr。以上所有容器的value都必须是自定义结构体。这些结构体都必须提供Fill()方法来完成填充。

        在Fill函数里面,要完成将表格里面单元格映射到对象。而单元格数据没有数据类型,全部都是string,要完成将单元格的数据映射到对象,就要定义好格式,然后解析字符串。sframe已经提供了默认的格式:

C++类型

格式

std::vecto、std::list、数组

形如:v1|v2|v3|v4

std::unordered_map、std::map

形如:k1#v1;k2#v2;k3#v3

数值、string

符合转换要求即可

3.       实例说明

(1)    将配置映射到单一自定义结构体对象

        比如说开发游戏,往往我们需要有一个全局配置来配置各种杂七杂八的东西。在程序里它就表现为一个结构体对象。至于文件格式,此种情况只支持json。

        JSON数据:

{    "num_field" : 10000,    "str_field" : "xiaoming",    "vec_field" : [1,2,3,4],    "map_field" : {        "1" : "str1",        "2" : "str2",        "3" : "str3",        "4" : "str4"    }}
        结构体应该是:

struct GlobalConfig{    int num_field;    std::string str_field;    std::vector<int> vec_field;    std::map<int, std::string> map_field;    void Fill(const json11::Json & reader)    {        JSON_FILLFIELD(num_field);        JSON_FILLFIELD(str_field);        JSON_FILLFIELD(vec_field);        JSON_FILLFIELD(map_field);    }};

Fill 函数的返回值也可以是bool类型的,用以表示加载是够成功。JSON_FILLFIELD是个宏,JSON_FILLFIELD(num_field)实际上等于sframe::Json_FillField(reader,”num_field” , this-> num_field);所以,要使用此宏,voidFill(const json11::Json & reader)函数的参数名必须是reader,且这个成员的变量名必须等于文件中的字段名。加载配置文件的代码:

bool LoadJson(){std::string content;if (!sframe::FileHelper::ReadFile("global.json", content)){return false;}std::string err;json11::Json json = json11::Json::parse(content, err);if (!err.empty()){return false;}// 加载GlobalConfig global_conf;if (!sframe::ConfigLoader::Load<const json11::Json>(json, global_conf)){return false;}return true;}
(2)    将配置映射到map

        除了全局配置这一类配置,大多数情况一个配置文件都分为若干条目,映射到c++中应该是map或者其他容器,这里只以map举例。对于这种多条目的,JSON和CSV都是支持的。比如我们需要一个英雄等级配置。

       若用JSON,数据文件应该是:

{[       {        “level” : 1,                    //等级“attk” : 100,                   //攻击力“title” : [“小白”, “初级战士”]     //称号},{        “level” : 2,                    //等级“attk” : 200,                   //攻击力“title” : [“一般”, “中级战士”]     //称号},{        “level” : 3,                    //等级“attk” : 300,                   //攻击力“title” : [“大神”, “高级战士”]     //称号}]}

        若用CSV,数据文件应该是:

可以填一些标识之类的东西

 

 

level

attk

title

本行填注释

 

 

类型(仅仅便于观察)

 

 

1

100

小白|初级战士

2

200

一般|中级战士

3

300

大神|高级战士


        结构体:

struct LevelConfig{    KEY_FIELD(int32_t,level);   int level;    int attk;    std::vector< std::string > title;    void Fill(const json11::Json & reader)    {        JSON_FILLFIELD(level);        JSON_FILLFIELD(attk);        JSON_FILLFIELD(title);    }    void Fill(sframe::TableReader & reader)    {        TBL_FILLFIELD (level);        TBL_FILLFIELD (attk);        TBL_FILLFIELD (title);    }};

        其中的KEY_FIELD主要用于指定这个结构体key的字段,用做map的key。展开就是定义了一个获取key的方法,int32_t GetKey() const {return level;}

        加载配置的代码如下(以CSV格式为例):

bool LoadCsv(){    std::string content;    if (!sframe::FileHelper::ReadFile("lv.csv", content))    {        return false;    }    sframe::Table tbl;    if (!sframe::CSV::Parse(content, tbl))    {        return false;    }    if (tbl.GetRowCount() < 1)    {        return false;    }    // 设置列名    int32_t column_count = tbl.GetColumnCount();    for (int i = 0; i < column_count; i++)    {        tbl.GetColumn(i).SetName(tbl[0][i]);    }    // 删除第一行,因为第一行是列名,不是数据    tbl.RemoveRow(0);    // TableReader    sframe::TableReader reader(tbl);    // 加载    std::map<int, LevelConfig> lv_conf;    if (!sframe::ConfigLoader::Load<sframe::TableReader>(reader, lv_conf))    {        return false;    }    return true;}

配置管理

        在实际项目中,配置文件是很多的。所以,将所有配置集中分类管理是很有必要的。sframe提供了ConfigSet类,用以统一加载、管理所有的配置。那么如何使用呢?我们还是以前面的例子来做说明。现欲将上面的两种配置都使用ConfigSet来做统一的加载与管理,GlobalConfig使用JSON,LevelConfig使用CSV。首先,ConfigSet的管理单元为配置模块,我们需要定义配置模块,sframe已提供了宏来辅助我们声明配置模块。如下:

OBJ_CONFIG_MODULE(GlobalConfigModule, GlobalConfig, 1);  MAP_CONFIG_MODULE(LevelConfigModule, int32_t, LevelConfig, 2);

        OBJ_CONFIG_MODULE声明一个单一对象配置模块,模块名为GlobalConfigModule,配置对象为GlobalConfig的对象,配置ID是1。

        MAP_CONFIG_MODULE声明一个map类型的配置模块,模块名为LevelConfigModule,配置对象为key为int,value为LevelConfig对象的map,配置ID是2。

        使用的代码如下:

sframe::ConfigSet conf_set;conf_set.RegistConfig< JsonLoader, GlobalConfigModule >("global.json");   // 参数为文件名conf_set.RegistConfig<TableLoader<CSV>, LevelConfigModule >("level.csv");std::vector<ConfigError> vec_err_info; // 错误信息conf_set->Load(“/data/conf”, &vec_err_info); //第一个参数指明配置文件路径// 查询全局配置模块std::shard_ptr<const GlobalConfigModule> global_conf = conf_set. GetConfigModule< GlobalConfigModule >();// 查询全局配置std::shard_ptr<const GlobalConfig > global_conf = conf_set. GetConfig< GlobalConfigModule >();// 查询等级配置模块std::shard_ptr<const LevelConfigModule> global_conf = conf_set. GetConfigModule< LevelConfigModule >();// 查询等级配置std::shard_ptr<const std::map<int, std::shard_ptr<LevelConfig>> map_lv_conf = conf_set. GetConfig< LevelConfigModule >();// 查询一条等级配置std::shard_ptr<const LevelConfig> lv_conf = conf_set. GetMapConfigItem<LevelConfigModule>(1);

        除了以上基本功能以外,sframe的配置管理模块还有以下功能: 

        (1)  配置模块的初始化

        在有些时候,我们在加载完一项配置之后,可能会需要做一些额外的处理。sframe提供了初始化机制来支持此项功能。此时我们需要自己声明配置模块

struct GlobalConfigModule : public sframe::ObjectConfigModule<GlobalConfig, 1>  {      void Initialize(sframe::ConfigSet & conf_set)      {          //Obj()方法获取std::shared_ptr<GlobalConfig>对象     }  };    struct LevelConfigModule : public sframe::MapConfigModule<int, LevelConfig, 2>  {      void Initialize(sframe::ConfigSet & conf_set)      {            //Obj()方法获取std::shared_ptr<std::map<T_Key, std::shared_ptr<LevelConfig>>对象       }}; 

        Initialize函数也支持返回bool类型的形式,若需返回bool,直接将void改成bool即可。ConfigSet在加载完所有配置后调用初始化函数。

        (2) 自定义将配置插入容器

        些时候我们在加载了一条配置以后,可能不希望单纯的将对象插入容器。可能使需要将对象分裂成几个对象,然后将几个对象全部保存到容器;或者我们要进行一些判断,成功才将其插入容器。sframe允许自定义将对象插入容器的方法。使用起来也很简单,我们只需为结构体添加PutIn成员函数。比如LevelConfig:

struct LevelConfig{bool PutIn(std::map<int, std::shard_ptr<LevelConfig>> & m){m[level] = *this;return true;}}

         本讲了大概,更多内容请参考源代码。

0 0