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