sylar源码阅读笔记-配置系统-核心函数解析

功能介绍

一个服务器框架系统中,通常需要做很多配置,例如日志系统的日志输出地,日志格式,端口号,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
待完善

校验方法,更新配置时会调用校验方法进行校验,以保证用户不会给配置项设置一个非法的值。
应该需要有导出当前配置的功能。
在多线程的情况下,线程不安全。

本文为本人学习过程中的笔记记录,不对的地方欢迎批评指针,一起交流~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值