配置模块

配置模块的作用

  • 统一管理配置项:通过集中式管理配置项,避免硬编码,便于维护与管理。
  • 支持多种类型配置:包括基础类型(如 int, string)、容器类型(如 vector, map)、以及自定义类型(如 CorsConfig)。
  • 支持 YAML 配置文件解析:便于配置文件的可读性和层级表达。
  • 支持运行时动态更新:变更配置项时触发回调函数,便于系统感知配置变更。

配置项基类

  • 整体架构

    ConfigVarBase

  • 解析

    • ConfigVarBase 是所有配置的基类,它主要有两个成员变量 m_namem_description 。前者便是每个配置都应该有的名称,后者表示对该配置的描述
    • ConfigVarBase 主要包含三个虚函数:toString()fromString()getTypeName(),用于给配置子类进行继承。前两个虚函数 toString()fromString()主要用于配置子类的序列化和反序列化,而后者的 getTypeName() 用于返回配置子类的类型

类型转换模板类

  • lexicalCast 的作用
    • 实现通用类型之间的转换,例如:
      • 将(配置文件内容YAML 字符串转换成某个具体的类型
        1
        2
        LexicalCast<std::string, int> cast;
        int val = cast("123");
      • 将某个类型转成 YAML 字符串(用于序列化配置
        1
        2
        LexicalCast<int, std::string> cast;
        std::string str = cast(456);
      • 支持复杂类型,如 vector<T>map<string, T> 等:可以通过模板特例化实现自定义转换逻辑,例如用 YAML 库处理容器类型。
  • 案例解析
    • 接下来,我将分析一组例子,使得大家对类型转换模板类的理解更加的透彻。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      /**
      * @brief 类型转换模板类偏特化(YAML String 转换成 std::vector<T>)
      */
      template<class T>
      class lexicalCast<std::string, std::vector<T>>
      {
      public:
      std::vector<T> operator() (const std::string& val)
      {
      YAML::Node node = YAML::Load(val);
      typename std::vector<T> vec;
      std::stringstream ss;
      for(size_t i = 0; i < node.size(); ++i)
      {
      ss.str("");
      ss << node[i];
      vec.push_back(lexicalCast<std::string, T>()(ss.str()));
      }
      return vec;
      }
      };
      • 在上述例子中,它主要的作用是:将一个 YAML 格式的字符串转换为 std::vector<T> 类型。
      • 什么是 YAML 格式的字符串:
        • 比如:简单的整数数组
          1
          ports: [8001, 8002, 8003]
        • 等价在 C++ 中传入的字符串形式就是
          1
          std::string val = "[8001, 8002, 8003]";
        • 这样通过 YAML::Node() 函数就可以将字符串 val 转换成 Node 结点,通过Node 结点,就可以将结点的内容全部输出到我们字节流中,最后转换成我们想要的类型。
    • 同理,将 vector<T> 转换成 std::string 就是相反的操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      template<class T>
      class lexicalCast<std::vector<T>, std::string>
      {
      public:
      std::string operator()(const std::vector<T> vec)
      {
      YAML::Node node(YAML::NodeType::Sequence);
      for(auto& i : vec)
      {
      node.push_back(YAML::Load(lexicalCast<T, std::string>()(i)));
      }
      std::stringstream ss;
      ss << node;
      return ss.str();
      }

      };
      • 现在我们想把一个 std::vector<T> 变成 YAML 字符串,比如:

        1
        std::vector<int> v = {1, 2, 3};

        转换后的结果

        1
        [1, 2, 3]
      • 而使用 YAML::Load(...) 是为了确保

        • 字符串 "1" 被当作 数字 1,而不是字符串 "1"
        • 字符串 "{x: 1, y: 2}" 被当作一个映射,而不是普通字符串
        • 字符串 "[1, 2]" 被当作数组,而不是字符串

配置子类

  • Configvar 的作用

    • ConfigVar 是一个模板类,属于配置系统中的核心组件,它的职责是:封装一个类型为 T 的配置变量,并提供
      • 类型安全的访问接口
      • 支持 YAML 字符串和变量间的双向转换
      • 支持配置变更时触发监听器回调
      • 统一管理和注册
    • 它继承自抽象基类 ConfigVarBase,用于实现多态存储。
    • 通过结合 lexicalCast 类型转换模板类,ConfigVar 实现了任何可序列化类型的灵活支持。
  • 整体框架

    ConfigVar

  • 解析

    • 成员变量
      1
      2
      T m_value;
      std::unordered_map<uint64_t, on_change_cb> m_cbs;
      • m_value 是当前配置的值
      • m_cbs 是监听器集合,支持注册多个回调,每个变化都会触发对应函数。
    • 重要函数
      • toString()
        • ConfigVar<T> 类中,我们希望能够支持任意类型 T 的配置参数将其序列化为 YAML 字符串形式,比如将一个 int、一个 std::vector<int> 甚至是 std::map<string, T> 转成 YAML 格式的字符串。
        • 为了做到这一点,toString() 并不硬编码转换逻辑,而是通过一个策略类 ToStr 来完成转换。这个 ToStr 实际上是一个 仿函数(函数对象),你可以把它理解为可以 “像函数一样使用的类”。
        • 核心代码
          1
          2
          3
          4
          5
          6
          7
          std::string toString() override {
          try
          {
          return ToStr()(m_value);
          }
          ...
          }
          这里的 ToStr()(m_value) 表示:创建一个 ToStr 类型的对象(默认为 lexicalCast<T, std::string>);然后调用它的 operator(),将 m_value 转换为 std::string
        • 流程图
                  ConfigVar::toString() 
                              ↓ 
                  调用 ToStr()(m_value) 
                              ↓ 
                  lexicalCast::operator()(const T& val) 
                              ↓ 
                  返回 YAML 格式的字符串
          
      • fromString()
        • ConfigVar<T> 类中,我们同样希望能够将一个 YAML 字符串形式的配置值反序列化成任意类型 T,例如从 "10" 恢复成 int,从 "[1, 2, 3]" 恢复成 std::vector<int>,甚至从复杂 YAML 字符串恢复成 std::mapstd::set 等容器。
        • 为了做到这一点,fromString() 并不直接写死转换逻辑,而是通过一个策略类 FromStr 来完成解析转换。这个 FromStr 本质上也是一个仿函数(函数对象),默认定义为 lexicalCast<std::string, T>
        • 核心代码:
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          bool fromString(const std::string& val) override 
          {
          try
          {
          setValue(FromStr()(val));
          }
          catch (const std::exception& e)
          {
          SYLAR_LOG_ERROR(...) << "exception: " << e.what();
          }
          return false;
          }
          这里的 FromStr()(val) 表示:创建一个 FromStr 类型的对象;调用其 operator(),将字符串 val 转换为类型 T;成功后通过 setValue() 更新当前配置值。
        • 流程图
                  ConfigVar::fromString(val) 
                              ↓ 
                      调用 FromStr()(val) 
                              ↓ 
          lexicalCast::operator()(const std::string& val) 
                              ↓ 
                      返回类型为 T 的值 
                              ↓ 
                  setValue() 更新配置项的值 
          

配置管理类

  • ConfigVarMap 的作用

    • 存储配置项
      • ConfigVarMapkey-value 的形式存储程序中的配置变量,方便统一管理
    • 统一访问入口
      • 通过 ConfigVarMap,程序中任何模块都能方便地读取或修改配置项,保证配置的集中和一致性。
    • 动态更新配置
      • 支持在程序运行时动态加载和修改配置,比如从配置文件读取,实时生效。
    • 监听配置变化
      • 有些实现支持监听配置项的变化,变更时自动触发回调,方便做动态调整。
  • 整体架构

    ConfigVarMap

  • 关键函数解析

    • GetDatas

      1
      2
      3
      4
      5
      static ConfigVarMap& GetDatas()
      {
      static ConfigVarMap s_datas;
      return s_datas;
      }

      ConfigVarMap 被定义为 std::unordered_map<std::string, ConfigVarBase::ptr>,主要用来查找配置变量的名字来获得对应的配置变量。

    • Lookup

      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
      template<class T>
      static typename ConfigVar<T>::ptr Lookup(const std::string& name, const std::string& description, const T& val)
      {
      auto it = GetDatas().find(name);
      if (it != GetDatas().end())
      {
      auto tmp = std::dynamic_pointer_cast<ConfigVar<T>>(it->second);
      if (tmp)
      {
      SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "Lookup name= " << name << "exists";
      return tmp;
      }
      else
      {
      SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name= " << name << " exists but type not"
      << TypeToName<T>() << " real type=" << it->second->getTypeName()
      <<" " << it->second->toString();
      }
      }

      if (name.find_first_not_of("abcdefghijklmnopqrstuvwxyz._0123456789") != std::string::npos)
      {
      SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Lookup name invalid " << name;
      throw std::invalid_argument(name);
      }

      typename ConfigVar<T>::ptr newConfigVar(std::make_shared<ConfigVar<T>>(name,description,val));
      GetDatas()[name] = newConfigVar;
      return newConfigVar;
      }

      该函数主要用来寻找是否有对应名称的参数,如果没有就创建配置参数。

    • ListAllMember

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      static void ListAllMember(const std::string& prefix,
      const YAML::Node& node,
      std::list<std::pair<std::string, const YAML::Node>>& output)
      {
      if (prefix.find_first_not_of("abcdefghijklmnopqrstuvwxyz._0123456789")
      != std::string::npos)
      {
      SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "Config invaild name : "<< prefix << " : " << node;
      return;
      }
      output.push_back(std::make_pair(prefix, node));
      if (node.IsMap())
      {
      for (auto it = node.begin(); it != node.end(); ++it)
      {
      ListAllMember(prefix.empty() ? it->first.Scalar() : prefix + "." + it->first.Scalar(), it->second, output);
      }

      }

      }
      • 函数作用
        • 该函数主要用于递归遍历 YAML 配置文件节点。
      • 参数解析
        • prefix:当前节点路径前缀
        • node:当前 YAML 节点
        • output:存储遍历结果,每个 pair 是一个“路径名-节点”的组合。
      • 函数解析
        • 先要判断 prefix 没有非法字符,防止路径名不合法。之后,如果 yaml 字符串不是 Map 类型,就直接将配置名称配置的值作为 pair 输入到 output。否则,递归处理 Yaml Map类型,遍历其所有子项。it->first.Scalar()keyit->second value。通过构造新的路径 new_prefix = prefix + “.” + 子节点名递归调用自己,继续往下遍历子节点。
      • 例子
        1
        2
        3
        4
        5
        6
        server:
        port: 8080
        host: 127.0.0.1
        database:
        user: root
        password: pass
        遍历后 output 中将包含:
        1
        2
        3
        4
        5
        6
        ("server", <Node>)
        ("server.port", 8080)
        ("server.host", "127.0.0.1")
        ("database", <Node>)
        ("database.user", "root")
        ("database.password", "pass")
    • LoadFromYaml

      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
      void Config::LoadFromYaml(const YAML::Node& root)
      {
      std::list<std::pair<std::string, const YAML::Node>> all_nodes;
      ListAllMember("",root,all_nodes);

      for (auto& i : all_nodes)
      {
      std::string key = i.first;
      if (key.empty())
      {
      continue;
      }
      std::transform(key.begin(), key.end(), key.begin(), ::tolower);
      ConfigVarBase::ptr var = LookupBase(key);
      if (var)
      {
      if (i.second.IsScalar())
      {
      var->fromString(i.second.Scalar());
      }
      else
      {
      std::stringstream ss;
      ss << i.second;
      var->fromString(ss.str());
      }
      }

      }
      }
      • 作用
        • YAML 配置文件中的数据,加载并解析为对应的配置变量(ConfigVarBase)实例的值。
      • 参数解析
        • root:是一个 YAML 的根节点,通常来自 YAML::Load() 解析出来的配置树。
      • 函数解析
        • 调用 ListAllMember() 把整个 YAML::Node 展开成一系列路径 + 值,保存到 all_nodes 列表中。
        • for (auto& i : all_nodes) 遍历每个键值对,尝试赋值。
        • 查找是否注册了这个 key 的变量(如 system.port),如果没有注册,则跳过(只加载已注册的配置项)
        • 最后解析 YAML 值并赋值。如果是标量(如 “myserver“、8080 等),直接调用 Scalar() 转字符串。如果是复杂类型(如 listmap ),则将整个 YAML::Node 流式输出为字符串,再传入 fromString()
    • Visit

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      void Config::Visit(std::function<void(ConfigVarBase::ptr)> cb) 
      {
      RWMutexType::ReadLock lock(GetMutex());
      ConfigVarMap& m = GetDatas();
      for(auto it = m.begin();it != m.end(); ++it)
      {
      cb(it->second);
      }

      }
      • 作用
        • 遍历所有配置项(已注册的 ConfigVarBase 派生对象),对每个变量执行传入的回调函数 cb
      • 参数解析
        • std::function<void(ConfigVarBase::ptr)> 就是对每个变量执行传入的回调函数。
      • 函数解析
        • RWMutexType::ReadLock:这是一个读写锁的读锁(Read Lock),可让多个线程同时读取配置数据。
        • GetMutex():返回用于保护配置数据的静态全局读写锁
        • 遍历每一个 std::pair<std::string, ConfigVarBase::ptr>。执行传入的 cb() 函数,参数是该配置变量对象。
      • 例子
        1
        2
        3
        4
        5
        6
        sylar::Config::Visit([](ConfigVarBase::ptr var) {
        std::cout << "name=" << var->getName()
        << " description=" << var->getDescription()
        << " type=" << var->getTypeName()
        << std::endl;
        });
        输出结果
        1
        2
        name=system.port description=system port type=int
        name=system.name description=system name type=string

整体框架图

Config

参考资料: