当前位置: 首页 > 工具软件 > SFrame > 使用案例 >

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

尹昂雄
2023-12-01

        开发游戏服务器,肯定会有大量的配置数据,什么等级配置、关卡配置等等。对于这些静态数据,我们一般都是采取文件的方式来存储的。策划事先配置好这些数据文件,服务器启动后便将其加载到内存,由配置管理模块统一管理。对于配置文件,有各种各样的格式,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;
}
}

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

 类似资料: