功能介绍
一个服务器框架系统中,通常需要做很多配置,例如日志系统的日志输出地,日志格式,端口号,IP地址等,这些配置可以写到程序里,也可以写到配置文件yaml里,写在yaml文件里更加方便,在系统启动时,将yaml文件里的配置加载到系统中,方便使用。
支持定义、声明配置项,使用yaml-cpp作为YAML解析库,从配置文件中加载用户配置,支持基本数据类型、STL容器、自定义复杂数据类型与YAML字符串的相互转换(使用仿函数、偏特化实现),支持配置变更通知。
主要逻辑:
从yaml文件中加载配置,然后设置到变量中。
加载文件使用YAML::LoadFile方法,会得到类似于树结构的结点root。然后遍历root及其子节点(个人认为该过程是解析key,将key保存下来,层级关系通过 点(.) 表示),解析成一个个非map形式(Scalar,Sequence),将值保存到<name,ConfigVar>的哈希表中。
ConfigVar类说明,包括name、val、description数据,包含转为字符串toString()和从字符串中加载fromString()方法。
当从文件中加载字符串值保存到对象中时,会用到fromString()方法,不同的复杂类型要实现不同的偏特化方法,具体实现是将字符串传入YAML::Load()方法中得到YAML::Node对象,然后遍历该对象,将值赋给变量。
当将对象以字符形式输出,注意该过程也是借用YAML::Node实现的,而不是直接转为字符串,为了和前面保持一致,是一种约定。
流程图(便于整体理解):
前置知识
YAML的基本数据类型和结点遍历
参考博客:yaml-cpp的实际使用注意事项_yaml-cpp将数据写入到string_力宁的博客-CSDN博客
YAML::Node有三种类型:Scalar、Sequence、Map
假设有以下yaml文件,内容如下:
user:
number: 5
info: {"x_1": 10, "x_2": 20, "x_3": 30, "x_4": 40, "x_5": 50}
我们可以通过yaml自带的YAML::LoadFile(filename)方法加载yaml文件:
YAML::Node root = YAML::LoadFile("/root/c_plus_plus_project/sylar/bin/conf/test.yaml");
// 为了直观看到root结点的形式,以及如何遍历YAML::Node结点,我们使用下面方法
print_yaml(root, 0);
// 详细定义如下,level表示层级
void print_yaml(const YAML::Node &node, int level)
{
/**
* node.Type()
* enum value { Undefined, Null, Scalar, Sequence, Map };
*/
if (node.IsScalar())
{
std::cout << std::string(level * 4, ' ') << node.Scalar() << " - " << node.Type() << std::endl;
}
else if (node.IsNull())
{
std::cout << std::string(level * 4, ' ') << "NULL"
<< " - " << node.Type() << std::endl;
}
else if (node.IsMap())
{
// 如果类型为map还要继续向下遍历
for (auto it = node.begin(); it != node.end(); it++)
{
// 输出键和值的类型
std::cout << std::string(level * 4, ' ') << it->first << " - " << it->second.Type() << std::endl;
// 继续遍历值
print_yaml(it->second, level + 1);
}
}
else if (node.IsSequence())
{
// 如果类型为list,需要遍历每个结点
for (size_t i = 0; i < node.size(); i++)
{
std::cout << std::string(level * 4, ' ') << i << " - " << node[i].Type() << std::endl;
print_yaml(node[i], level + 1);
}
}
}
// 输出结果
/**
user - 4
number - 2
5 - 2
info - 4
x_1 - 2
10 - 2
x_2 - 2
20 - 2
x_3 - 2
30 - 2
x_4 - 2
40 - 2
x_5 - 2
50 - 2
*/
包含类及作用
只列出主要的函数,函数细节见代码
ConfigVarBase
class ConfigVarBase{
protected:
std::string m_name; // 名称
std::string m_description; // 描述
public:
virtual std::string toString() = 0; // 将值转为string类型
virtual bool fromString(const std::string &val) = 0; // 从string中加载值
};
ConfigVar
配置变量包含三个属性:name,value,description
// 支持类型转换
// (自定义类型/stl类型/简单类型) -> string
// string -> (自定义类型/stl类型/简单类型)
template <class T,
class FromStr = LexicalCast<std::string, T>,
class ToStr = LexicalCast<T, std::string>
>
class ConfigVar:public ConfigVarBase{
public:
std::string toString() override{} // return ToStr()(m_val);
bool fromString(const std::string &val) override{} // setValue(FromStr()(val));
const T getValue() const{}
void setValue(const T &val);
private:
T m_val; // 配置的值为value
};
Config
class Config{
private:
// 存放变量 <name,ConfigVarBase::ptr>
// 键为string类型,对于yaml中具有层级关系的使用 . 表示,例如:
// user.number
// user.info
std::map<std::string, ConfigVarBase::ptr> s_datas;
public:
// 根据name查找ConfigVar,如果找到则返回查找结果,没找到则插入该值
static ConfigVar<T>::ptr Lookup(const std::string &name,
const T &default_value,
const std::string &description = "");
// 根据name查找ConfigVar
static ConfigVar<T>::ptr Lookup(const std::string &name);
// 从yaml文件中加载配置
static void LoadFromYaml(const YAML::Node &root);
static ConfigVarBase::ptr LookupBase(std::string &name);
};
核心函数
ListAllMember
函数作用,对YAML::Node结点进行解析,解析若干个键值对的形式 string : YAML::Node,这里的Node为子节点(非map类型)。键是从根节点到该结点一次访问的字符串(中间用点隔开),值为相应的数据。
示例演示:
假设yaml内容为:
user:
number: 5
info: {"x_1": 10, "x_2": 20, "x_3": 30, "x_4": 40, "x_5": 50}
调用如下函数:
YAML::Node root = YAML::LoadFile("/root/c_plus_plus_project/sylar/bin/conf/test.yaml");
// 结点类型<string,YAML::Node>
std::list<std::pair<std::string, const YAML::Node>> all_nodes;
// 将root中的结点进行解析,存放到all_nodes中
ListAllMember("", root, all_nodes);
// 遍历输出all_nodes
for (auto it = all_nodes.begin(); it != all_nodes.end(); it++)
{
std::cout << it->first << std::endl;
}
// 输出结果如下:
/*
此处是空格
user
user.number
user.info
user.info.x_1
user.info.x_2
user.info.x_3
user.info.x_4
user.info.x_5
*/
// 上边的每个字符串都是键,对应的值存放Node结点
解析完后,方便将键值对插入到配置中。
从YAML结点中加载设置变量:Config::LoadFromYaml
首先,YAML结点是使用YAML::LoadFile函数解析yaml文件获得的,该函数作用主要时将yaml文件内容加载近来,然后解析成多个不包含map形式的结点,保存到all_nodes中。然后遍历all_nodes,对系统中已经有的值进行更新,如果没有该值则忽略(我感觉如果系统中目前不存在该值,也可以将该值插入)
YAML::Node root=YAML::LoadFile(filename);
void Config::LoadFromYaml(const YAML::Node &root)
{
// 结点类型<string,YAML::Node>
std::list<std::pair<std::string, const YAML::Node>> all_nodes;
// 将root中的结点进行解析,存放到all_nodes中
ListAllMember("", root, all_nodes);
for (auto &i : all_nodes)
{
// 遍历,获取key,查找是否包含key,如果包含,将之前修改为从文件中加载的值
std::string key = i.first;
if (key.empty())
{
continue;
}
// 将key转为小写
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
// 查询是否包含key
ConfigVarBase::ptr var = LookupBase(key);
// 如果存在key才从文件中加载更新,不存在直接跳过
if (var)
{
if (i.second.IsScalar())
{
// 将YAML::内结点值转为Scalar类型
// 然后从字符串中加载(已通过实现偏特化实现了类型的转换),设置m_val,进行更新
var->fromString(i.second.Scalar());
}
else
{
// 其他类型 Sequence,偏特化中fromString有对应的处理方法
std::stringstream ss;
ss << i.second;
var->fromString(ss.str());
}
}
}
}
类型转换
// 主要使用偏特化的思想
// 基本数据类型string
template <class F, class T>
class LexicalCast
{
public:
// 重载操作符,将v转为T类型
T operator()(const F &v)
{
return boost::lexical_cast<T>(v);
}
};
// string 转 vector
template <class T>
class LexicalCast<std::string, std::vector<T>>
// vector 转 string
template <class T>
class LexicalCast<std::vector<T>, std::string>
// string 转 list
template <class T>
class LexicalCast<std::string, std::list<T>>
// list 转 string
template <class T>
class LexicalCast<std::list<T>, std::string>
// 其余还有set\unordered_set\map\unordered_map
自定义类型转换,说白了就是实现偏特化方法,写清楚转换的规则。
class Person
{
public:
std::string m_name = "";
int m_age = 0;
bool m_sex = 0;
std::string toString() const
{
std::stringstream ss;
ss << "[Person name=" << m_name
<< "age=" << m_age
<< "sex=" << m_sex << "]";
return ss.str();
}
};
namespace sylar
{
// ***************person start****************
/**
* 数据类型转换-person偏特化版本
* string -> person
*/
template <>
class LexicalCast<std::string, Person>
{
public:
Person operator()(const std::string &v)
{
// loads the input string as a single YAML document
YAML::Node node = YAML::Load(v);
Person p;
p.m_name = node["name"].as<std::string>();
p.m_age = node["age"].as<int>();
p.m_sex = node["sex"].as<bool>();
return p;
}
};
/**
* 数据类型转换-Person偏特化版本
* Person -> string
*/
template <>
class LexicalCast<Person, std::string>
{
public:
std::string operator()(const Person &v)
{
YAML::Node node;
node["name"] = v.m_name;
node["age"] = v.m_age;
node["sex"] = v.m_sex;
std::stringstream ss;
ss << node;
return ss.str();
}
};
// ***************person end****************
}
配置变更事件
支持给配置项注册配置变更通知(也就是配置发生变化了,让程序知道配置发生变化了,然后去做某些操作)。比如对于网络服务器而言,如果服务器端口配置变化了,那程序应该重新起监听端口。这个功能通过注册回调函数来实现的,配置使用方预先给配置项注册一个配置变更回调函数,配置项发生变化时,触发对应的回调函数以通知调用方**。**由于一项配置可能在多个地方引用,所以配置变更回调函数应该是一个数组的形式(存为map形式,挨个监听函数通知,类似于观察者模式)。
具体实现如下:
// 在类ConfigVar中添加map:<key,回调函数> uint64_t key,要求唯一,一般可以用hash值
typedef std::function<void(const T &old_value, const T &new_value)> on_change_cb;
std::map<uint64_t, on_change_cb> m_cbs;
// 添加相应的函数
// 增加监听
void addListener(uint64_t key, on_change_cb cb)
{
m_cbs[key] = cb;
}
// 删除监听
void delListener(uint64_t key)
{
m_cbs.erase(key);
}
// 获得监听器
on_change_cb getListener(uint64_t key)
{
auto it = m_cbs.find(key);
return it == m_cbs.end() ? nullptr : it->second;
}
// 清空监听器
void clearListener()
{
m_cbs.clear();
}
刚刚提到说,值发生变化的时候,去执行相应的回调函数,也就是在setValue函数中实现:
/**
* 设置值的时候,监听值是否发生,如果发生变化,做相应的操作
*/
void setValue(const T &val)
{
// 这里有比较运算,需要在自定义类中重载 ==
if (val == m_val)
{
return;
}
for (auto &i : m_cbs)
{
// 挨个执行回调函数,类似于观察者模式
i.second(m_val, val);
}
// 赋值
m_val = val;
}
使用代码如下:
sylar::ConfigVar<Person>::ptr g_person = sylar::Config::Lookup("class.person", Person(), "class person");
// 使用lambda表达式定义回调函数
g_person->addListener(10, [](const Person &old_value, const Person &new_value)
{ SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "listener function"
<< "old value= " << old_value.toString()
<< "new value " << new_value.toString(); });
YAML::Node root = YAML::LoadFile("/root/c_plus_plus_project/sylar/bin/conf/log.yaml");
sylar::Config::LoadFromYaml(root);
SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << g_person->getValue().toString() << "\n"
<< g_person->toString();
语法函数知识点记录
boost::lexical_cast
实现值到字符串之间的转换。
typename
如果将依赖模板参数的名称作为类型使用,就需要使用typename来修饰,告诉编译器这是一个类型
template<class T>
void foo(){
typename T::iterator *iter;
}
find_first_not_of()
正向查找在原字符串中第一个与指定字符串(或字符)中的任一字符都不匹配的字符,返回它的位置。若查找失败,则返回npos。(npos定义为保证大于任何有效下标的值。)
string str=“abcdefab”;
cout<<str.find_first_not_of(‘h’)<<endl;//第二个参数为0,默认从原串下标为0开始查找。第一个a就和带查字符不同,故输出a的下标0。
cout<<str.find_first_not_of(“twj”,1)<<endl;//从下标为1开始查,第一个b就和待查子串任一字符不同,故输出b的下标1。
std::transform
// 转为小写
string m_name="ABc";
std::transform(m_name.begin(), m_name.end(), m_name.begin(), ::tolower);
cout<<m_name; // abc
待完善
校验方法,更新配置时会调用校验方法进行校验,以保证用户不会给配置项设置一个非法的值。
应该需要有导出当前配置的功能。
在多线程的情况下,线程不安全。
本文为本人学习过程中的笔记记录,不对的地方欢迎批评指针,一起交流~~