Velox_控制模块

概述

配置模块的作用为提供一个类型安全、集中式管理应用程序配置项的方式,允许开发者定义任意类型的配置变量,为它们设置默认值,并通过YAML文件方便地加载和修改这些配置。

该模块的核心设计思想是将每一个配置项抽象为一个ConfigVar对象。所有这些对象都由一个单例管理的Config类进行统一存储和访问。这种设计避免了配置项散落在代码各处,使得配置的管理、维护和查阅变得非常方便。

该模块主要由以下几部分构成:

  1. ConfigVarBase: 一个抽象基类,定义了所有配置变量的通用接口,如获取名称、描述、类型名以及与字符串的双向转换(序列化/反序列化)。
  2. ConfigVar<T>: 继承自ConfigVarBase的模板类。它是配置变量的具体实现,能够存储任意类型 T 的值。它还实现了一个回调机制,当配置值发生变化时,可以通知所有监听者。
  3. TypeConverter: 一系列模板类及其偏特化,用于在各种C++类型(包括基本类型、STL容器如vector, list, map, set等)和字符串之间进行转换。该模块默认使用 boost::lexical_cast 进行基础类型转换,并为复杂容器类型提供了基于 yaml-cpp 的序列化和反序列化实现。对于自定义类型,只需完成对应的偏特化即可进行转换。
  4. Config: 一个管理类,它包含一个静态的unordered_map用于存储所有的配置变量。它提供了全局的静态方法来创建、获取和加载配置项,是与本模块交互的主要入口。

本模块通过yaml-cpp库解析YAML文件,再将从.yml中解析出来的结构化数据更新到对应的ConfigVar变量中。

主要特性

  • 类型安全: 每个配置项在定义时都强绑定一个类型,在获取时会进行动态类型检查,防止了类型不匹配导致的运行时错误。
  • 集中式管理: 所有配置项都通过一个全局的Config管理器进行注册和访问,便于追踪和管理。
  • 支持复杂数据结构: 内置了对 std::vector, std::list, std::set, std::unordered_set, std::map, std::unordered_map 等常用STL容器的序列化和反序列化支持。
  • 基于YAML: 使用广泛流行的YAML格式作为配置文件,语法清晰,支持复杂数据结构。
  • 动态更新与回调: 支持在运行时通过加载YAML文件来更新配置值。当某个配置值发生变化时,可以触发预先注册的回调函数,使应用程序能够动态地响应配置变化。

重要函数介绍

Config类

  1. getDatas()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    using ConfigVarMap = std::unordered_map<std::string, ConfigVarBase::ptr>;
    /**
     * @brief 返回所有的配置项
     */
    static ConfigVarMap& getDatas()
    {
      static ConfigVarMap s_datas;
      return s_datas;
    }

    这里的数据存储的是基类指针ConfigVarBase::ptr,这是典型的多态设计。因为ConfigVar<T> 是模板类,如果用的是std::unordered_map<std::string, ConfigVar<T>::ptr>;则只能放一种类型的配置变量(比如 intstd::string)。

  2. getOrCreateConfigVarPtr(const std::string& name, const T& default_value, const std::string& description = "")

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    /**
     * @brief 获取/创建对应参数名的配置参数
     * @param[in] name 配置参数名称
     * @param[in] default_value 参数默认值
     * @param[in] description 参数描述
     * @details 获取参数名为name的配置参数,如果存在则直接返回
     *          如果不存在, 则根据参数创建对应的配置参数
     * @return 返回对应的配置参数指针, 如果参数名存在但是类型不匹配则返回 nullptr
     * @exception 如果参数名为空或包含非法字符[^0-9a-z_.] 抛出异常 std::invalid_argument
     */
    template<class T>
    static typename ConfigVar<T>::ptr
    getOrCreateConfigVarPtr(const std::string& name, const T& default_value, const std::string& description = "")
    {
      // getDatas() 返回的是存储全部配置项的 unordered_map
      auto it = getDatas().find(name);
      if (it != getDatas().end())
      {
        auto tmp = std::dynamic_pointer_cast<ConfigVar<T>>(it->second);
        if (tmp)
        {
          VELOX_INFO("ConfigVar [name:{}, valueType:{}] exists", name, tmp->getTypeName());
          return tmp;
        }
    
        // 存在但类型不匹配
        VELOX_ERROR(
            "ConfigVar name {} exists but type not {}, real type is {}:{}",
            name,
            velox::util::typeName<T>(),
            it->second->getTypeName(),
            it->second->toString());
        return nullptr;
      }
    
      // 创建配置参数
      if (!velox::util::isValidName(name))
      {
        VELOX_ERROR("ConfigVar name invalid: {}", name);
        throw std::invalid_argument(name);
      }
    
      auto var = std::make_shared<ConfigVar<T>>(name, default_value, description);
      getDatas()[name] = var;
      return var;
    }
  3. loadFromYaml(const YAML::Node& root)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
    /**
    * @brief 使用 YAML::Node 更新对应配置参数
    * @details 只会更新已经存在的配置参数, 未存在的配置参数不会创建, 只会在日志中
    *          记录
    * @param[in] root 表示当前要解析的 YAML::Node 结点
    */
    void Config::loadFromYaml(const YAML::Node& root)
    {
        // all_nodes 用于存储解析出来的 YAML::Node
        std::vector<std::pair<const std::string, const YAML::Node>> all_nodes;
    
        /**
        * @brief 递归地将一个YAML节点扁平化一系列"点分路径"键和对应节点的键值对
        * @details 递归遍历YAML节点, 构造以点号连接的键路径, 并将每个路径及其对应
        *          的 YAML::Node 添加到输出列表中, 从而实现将 YAML 的层级结构扁平化. 
        *          只处理结点为 Map 的情况, 不会处理 Sequence (避免过度扁平不利于管理).
        * 第一个参数为 当前键路径的前缀
        * 第二个参数为 当前处理的 YAML 节点
        * 第三个参数为 存储扁平化结果
        */
        listAllMembers("", root, all_nodes);
    
        // 依次处理每个 YAML::Node
        for (const auto& node_pair : all_nodes)
        {
        // 获取该配置变量指针
        const std::string& key = node_pair.first;
        ConfigVarBase::ptr var_ptr = getConfigVarBasePtr(key);
    
        // 判断该 key 是否已经注册, 注册则更新其值
        if (var_ptr)
        {
            // 经过前面的 listAllMembers 处理, 这里的 YAML::Node 只会是标量或列表
            if (node_pair.second.IsScalar())
            {
            var_ptr->fromString(node_pair.second.Scalar());
            }
            else
            {
            std::stringstream ss;
            ss << node_pair.second;
            var_ptr->fromString(ss.str());
            }
        }
        else
        {
        // 没注册的配置, 则写入日志
            VELOX_WARN("Unrecognized config key: {}", key);
        }
        }
    }
  4. loadFromConfDir(const std::string& relative_dir_path, bool force = false)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    /**
    * @brief 加载指定项目子目录里面的配置文件
    * @details 根据 force 参数来判断是否强制加载全部配置文件
    *          force == false时, 只加载更新过的配置文件
    *          force == true时, 加载指定目录下的全部配置文件
    */
    void Config::loadFromConfDir(const std::string& relative_dir_path, bool force)
    {
        // 获取项目根目录下的相对目录路径下的扩展为 .yml 的全部文件的绝对路径
        // 假设项目根路径为: /home/xxx/velox, relative_dir_path = config
        // 那么就会去 /home/xxx/velox/config 这个目录下找到所有 .yml 文件, 并将它们
        // 的绝对路径一起返回(以vector的形式返回).
        auto files = velox::util::listAllFilesByExt(relative_dir_path, ".yml");
    
        // 依次处理每个配置文件
        for (const auto& file : files)
        {
        // 获取该文件的最新写入时间
        std::error_code ec;
        auto last_write_time = fs::last_write_time(file, ec);
        if (ec)
        {
            VELOX_WARN("Skip config file '{}': failed to get last write time: {}", file.string(), ec.message());
            continue;
        }
    
        auto current_timestamp = velox::util::toUnixTimestamp(last_write_time);
        auto& cached_timestamp = getCachedTimestampForFile(file);
    
        // 在 force == false 且该文件后续没有被修改时 跳过该配置文件
        if (!force && current_timestamp == cached_timestamp)
        {
            VELOX_INFO("Skip config file '{}': unchanged since last load.", file.string());
            continue;
        }
    
        try
        {
            cached_timestamp = current_timestamp;  // 将该文件最新写入时间进行缓存
            YAML::Node root = YAML::LoadFile(file);
            loadFromYaml(root);  // 调用该函数对 YAML::Node 进行解析并更新存储的值
            VELOX_INFO("Loaded config file '{}'", file.string());
        }
        catch (const std::exception& e)
        {
            VELOX_ERROR("Failed to load config file '{}': {}", file.string(), e.what());
        }
        catch (...)
        {
            VELOX_ERROR("Failed to load config file '{}': unknown error.", file.string());
        }
        }
    }

ConfigVar类

该类用于定义配置参数结构, 除了实现 ConfigVarBase 的虚函数外, 还新增了一些函数, 其中比较重要的有:

  1. setValue(const T& value)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
        /**
        * @brief 设置当前参数的值
        * @details 当参数的值发生变化时会调用该变量所对应的变更回调函数
        */
        void setValue(const T& value)
        {
        // 判断新旧值是否相同
        if (value == m_val)
        {
            return;
        }
    
        // 更新值
        T old_val = m_val;
        m_val = value;
    
        // m_cbs 是一个 unordered_map, 用于存储该配置参数的全部回调函数
        for (const auto& [id, cb] : m_cbs)
        {
            // 调用注册过的回调函数
            cb(old_val, m_val);
        }
        }

    使用回调函数机制可以在该配置参数的值发生变化时进行一些处理(比如更新log模块的对应参数等)。

  2. addListener(on_change_cb cb)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    // using on_change_cb = std::function<void(const T& old_val, const T& new_val)>;
    // std::unordered_map<uint64_t, on_change_cb> m_cbs;
    
        /**
        * @brief 添加变化回调函数
        * @return 返回该回调函数对应的唯一 id, 用于删除回调
        */
        uint64_t addListener(on_change_cb cb)
        {
        // 自动生成该回调函数的 key
        static uint64_t s_fun_id = 0;
        ++s_fun_id;
    
        m_cbs[s_fun_id] = cb;
        return s_fun_id;
        }

使用示例

Config模块的使用都是通过调用Config类中的静态成员函数来实现。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include "config.hpp"
#include <vector>

// 定义一个结构体
struct ServerDefine
{
    std::vector<std::string> address;
    int keepalive = 0;  // 可选, 默认 0
    int timeout = 1000;
    std::string name;
    std::string accept_worker;
    std::string io_worker;
    std::string process_worker;
    std::string type;

    bool operator==(const ServerDefine& other) const
    {
      return address == other.address && keepalive == other.keepalive && timeout == other.timeout && name == other.name &&
             accept_worker == other.accept_worker && io_worker == other.io_worker &&
             process_worker == other.process_worker && type == other.type;
    }
};

// 实现对应的偏特化, 用于 YAML:Node 和 ServerDefine 类型之间的转换
namespace velox::config
{
  // string -> ServerDefine
  template<>
  class TypeConverter<std::string, ServerDefine>
  {
   public:
    ServerDefine operator()(const std::string& v)
    {
      YAML::Node node = YAML::Load(v);
      ServerDefine def;

      if (node["address"].IsDefined())
      {
        def.address = node["address"].as<std::vector<std::string>>();
      }
      else
      {
        VELOX_ERROR("server address list is null");
        throw std::logic_error("server address list is null");
      }

      if (node["keepalive"].IsDefined())
      {
        def.keepalive = node["keepalive"].as<int>();
      }

      if (node["timeout"].IsDefined())
      {
        def.timeout = node["timeout"].as<int>();
      }

      if (node["name"].IsDefined())
      {
        def.name = node["name"].as<std::string>();
      }
      else
      {
        VELOX_ERROR("server name is empty");
        throw std::logic_error("server name is empty");
      }

      if (node["accept_worker"].IsDefined())
      {
        def.accept_worker = node["accept_worker"].as<std::string>();
      }
      else
      {
        VELOX_ERROR("server accept worker is empty");
        throw std::logic_error("server accept worker is empty");
      }

      if (node["io_worker"].IsDefined())
      {
        def.io_worker = node["io_worker"].as<std::string>();
      }
      else
      {
        VELOX_ERROR("server io worker is empty");
        throw std::logic_error("server io worker is empty");
      }

      if (node["process_worker"].IsDefined())
      {
        def.process_worker = node["process_worker"].as<std::string>();
      }
      else
      {
        VELOX_ERROR("server process worker is empty");
        throw std::logic_error("server process worker is empty");
      }

      if (node["type"].IsDefined())
      {
        def.type = node["type"].as<std::string>();
      }
      else
      {
        VELOX_ERROR("server type is empty");
        throw std::logic_error("server type is empty");
      }

      return def;
    }
  };

  // ServerConfig -> string
  template<>
  class TypeConverter<ServerDefine, std::string>
  {
   public:
    std::string operator()(const ServerDefine& def)
    {
      YAML::Node node;

      node["address"] = def.address;
      if (def.keepalive != 0)
      {
        node["keepalive"] = def.keepalive;
      }
      node["timeout"] = def.timeout;
      node["name"] = def.name;
      node["accept_worker"] = def.accept_worker;
      node["io_worker"] = def.io_worker;
      node["process_worker"] = def.process_worker;
      node["type"] = def.type;

      std::stringstream ss;
      ss << node;
      return ss.str();
    }
  };

}  // namespace velox::config

int main()
{
    // 先注册配置变量
  auto servers_config = Config::getOrCreateConfigVarPtr<std::vector<velox::test::ServerDefine>>(
      "servers", std::vector<velox::test::ServerDefine>(), "servers config");
    return 0;

    // 添加一个回调函数(可选)
    auto cb_id = servers_config->addListener(
      [](const std::vector<ServerDefine>& old_val, const std::vector<ServerDefine>& new_val)
      { std::cout << "servers value change.\n"; });

   // 构造 YAML 内容
  std::string yaml_content = R"(
servers:
  - address: ["0.0.0.0:8090", "127.0.0.1:8091", "/tmp/test.sock"]
    keepalive: 1
    timeout: 1000
    name: sylar/1.1
    accept_worker: accept
    io_worker: http_io
    process_worker: http_io
    type: http
  - address: ["0.0.0.0:8062", "0.0.0.0:8061"]
    timeout: 1000
    name: sylar-rock/1.0
    accept_worker: accept
    io_worker: io
    process_worker: io
    type: rock)";

  // 解析 YAML
  YAML::Node root = YAML::Load(yaml_content);
  Config::loadFromYaml(root); // 这里servers_config的值会发生变化, 触发回调函数
}

更多使用示例,可查看test/config/config_test.cpp文件

0%