【视频点播系统】Etcd-SDK 介绍及使用

Etcd-SDK 介绍及使用

  • [一. Etcd 介绍](#一. Etcd 介绍)
  • [二. Etcd 安装](#二. Etcd 安装)
  • [三. etcd-cpp-apiv3 SDK 介绍](#三. etcd-cpp-apiv3 SDK 介绍)
  • [四. etcd-cpp-apiv3 SDK 安装](#四. etcd-cpp-apiv3 SDK 安装)
  • [五. etcd-cpp-apiv3 类与接口](#五. etcd-cpp-apiv3 类与接口)
    • [1. Value、Event、Response](#1. Value、Event、Response)
    • [2. Client、KeepAlive、Watcher](#2. Client、KeepAlive、Watcher)
  • [六. etcd-cpp-apiv3 使用样例](#六. etcd-cpp-apiv3 使用样例)
    • [1. 目录结构](#1. 目录结构)
    • [2. 项目构建](#2. 项目构建)
    • [3. 代码实现](#3. 代码实现)
      • [3.1 简单数据操作](#3.1 简单数据操作)
      • [3.2 目录操作](#3.2 目录操作)
  • [七. etcd-cpp-apiv3 封装](#七. etcd-cpp-apiv3 封装)
    • [1. 设计与实现](#1. 设计与实现)
      • [1.1 目录结构](#1.1 目录结构)
      • [1.2 代码实现](#1.2 代码实现)
    • [2. 使用样例](#2. 使用样例)
      • [2.1 目录结构](#2.1 目录结构)
      • [2.2 项目构建](#2.2 项目构建)
      • [2.3 代码实现](#2.3 代码实现)
  • [八. Etcd 结合 BRpc 使用样例](#八. Etcd 结合 BRpc 使用样例)
    • [1. 目录结构](#1. 目录结构)
    • [2. 项目构建](#2. 项目构建)
    • [3. 代码实现](#3. 代码实现)

一. Etcd 介绍

  • etcd 是一个 高可用、强一致性的分布式键值存储系统,由 CoreOS 开源,常用于分布式系统中的 配置管理、服务注册与发现、分布式锁 等场景。它基于 Raft 一致性算法,通过多节点复制日志的方式,保证数据在集群中的一致性和可靠性。
  • etcd 以 Key-Value 的形式存储数据,支持版本控制、事务操作和监听机制(Watch),当数据发生变化时可以实时通知客户端。etcd 提供了 HTTP/JSON 和 gRPC 接口,使用简单,性能稳定。
  • 由于具备强一致性和良好的可用性,etcd 被 Kubernetes 等系统作为核心组件,用来保存集群状态和关键元数据,是现代云原生和微服务架构中的重要基础设施。

二. Etcd 安装

bash 复制代码
# 更新软件源
sudo apt-get update
# 安装 Etcd
sudo apt-get install etcd
# 启动 Etcd 服务
sudo systemctl start etcd
# 设置 Etcd 开机自启
sudo systemctl enable etcd

由于在环境搭建中已经安装了 Etcd 服务器,并且启动了,这里不需要再次执行了。

三. etcd-cpp-apiv3 SDK 介绍

  • etcd-cpp-apiv3 是一个 基于 etcd v3 API 的 C++ 客户端库,用于在 C++ 程序中访问和管理 etcd 分布式键值存储。它对 etcd 的 HTTP/JSON 接口进行了封装,底层依赖 cpprestsdk,为 C++ 开发者提供了相对简洁、易用的接口。
  • 通过 etcd-cpp-apiv3,开发者可以方便地完成常见操作,如 键值对的增删改查 (put/get/delete)、租约 (Lease) 管理、KeepAlive 自动续约 以及 Watcher 监听键变化。这些功能常用于服务注册与发现、配置中心、分布式锁等场景。
  • 该库采用 异步模型,接口通常返回 pplx::task<Response>,既支持异步调用,也可以通过 .get() 转为同步使用。整体设计偏工程化,适合在微服务和分布式系统的 C++ 项目中集成使用。

四. etcd-cpp-apiv3 SDK 安装

etcd 采用 golang 编写,v3 版本通信采用 grpc API,即 (HTTP2+protobuf),官方只维护了 go 语言版本的 client 库,因此需要找到 C/C++ 非官方的 client 开发库 etcd-cpp-apiv3,是一个 etcd 的 C++ 版本客户端 API。它依赖于 mipsasm、boost、protobuf、gRPC、cpprestsdk等库。

etcd-cpp-apiv3 的 GitHub 地址是:https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3

先安装依赖

bash 复制代码
sudo apt-get install libboost-all-dev libssl-dev
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc
sudo apt-get install libgrpc-dev libgrpc++-dev
sudo apt-get install libcpprest-dev

安装 etcd-cpp-apiv3

bash 复制代码
# 下载源码
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
# 切换目录
cd etcd-cpp-apiv3
mkdir build
cd build
# 生成 Makefile
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
# 编译代码
make -j$(nproc)
# 安装
sudo make install

由于在环境搭建中已经安装了 etcd-cpp-apiv3,这里不需要再次安装了。

五. etcd-cpp-apiv3 类与接口

1. Value、Event、Response

cpp 复制代码
namespace etcd {
    // Value 类表示 etcd 中的键值对
    class Value {
        bool is_dir(); // 判断是否为目录
        std::string const& key(); // 获取键值对的key值
        std::string const& as_string(); // 获取键值对的val值
        int64_t lease(); // 获取租约ID
    };
    // Event 类表示 etcd 中的事件
    class Event {
        enum class EventType { // 事件类型
            PUT, // 键值对新增或数据发生改变
            DELETE_, // 键值对被删除
            INVALID, // 无效事件类型
        };
        EventType event_type() const; // 获取事件类型
        const Value& kv() const; // 当前键值对的数据 
        const Value& prev_kv() const; // 改变前键值对的数据 
    };
    // Response 类表示 etcd 中的响应
    class Response {
        bool is_ok() const; // 判断响应是否成功
        std::string const& error_message() const; // 获取错误信息
        Value const& value() const; // 当前的数值/请求的处理结果 
        Value const& prev_value() const; // 之前的数值 
        Value const& value(int index) const; // 获取第index个数值 
        std::vector<Event> const& events() const; // 触发的事件 
        using Values = std::vector<Value>;
        Values const& values() const; // 多组数据的响应结果--针对目录
    };
}

2. Client、KeepAlive、Watcher

cpp 复制代码
namespace etcd {
    // Client 类表示 etcd 客户端
    class Client {
        // 构造函数,使用 etcd 服务器地址初始化客户端
        Client(std::string const& etcd_url, std::string const& load_balancer = "round_robin");
        // pplx::task 并行库异步结果对象 
        // 阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果 
        // 非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态 
        pplx::task<Response> put(std::string const& key, std::string const& value); // 新增或修改键值对
        pplx::task<Response> put(std::string const& key, std::string const& value, const int64_t leaseId); // 新增或修改键值对并绑定租约
        pplx::task<Response> get(std::string const& key); // 获取键值对
        pplx::task<Response> ls(std::string const& key); // 获取目录下的所有键值对
        pplx::task<Response> leasegrant(int ttl); // 申请租约
        pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(int ttl); // 保活租约
        pplx::task<Response> rm(std::string const& key); // 删除键值对
        pplx::task<Response> rmdir(std::string const& key, bool recursive = false); // 删除目录
        pplx::task<Response> watch(std::string const& key, bool recursive = false); // 监控键值对变化
        pplx::task<Response> leaserevoke(int64_t lease_id); // 撤销租约
        pplx::task<Response> lock(std::string const& key); // 加锁
        pplx::task<Response> unlock(std::string const& lock_key); // 解锁
        pplx::task<Response> txn(etcdv3::Transaction const& txn); // 执行事务
    };
    // KeepAlive 类表示 etcd 中的保活对象,当租约过期时,会触发异常处理函数
    class KeepAlive {
        // 构造函数,使用客户端、TTL和租约ID初始化保活对象
        KeepAlive(Client const& client, int ttl, int64_t lease_id = 0);
        // 构造函数,使用地址、异常处理函数、TTL和租约ID初始化保活对象
        KeepAlive(std::string const& address, std::function<void(std::exception_ptr)> const& handler, int ttl,int64_t lease_id = 0);
        int64_t Lease(); // 获取租约ID
        void Cancel(); // 取消保活
    };
    // Watcher 类表示 etcd 中的监控对象,当键值对变化时,会触发回调函数
    class Watcher {
        // 构造函数,使用客户端、键值对key、回调函数和是否递归监控初始化监控对象
        Watcher(Client const& client, std::string const& key, std::function<void(Response)> callback, bool recursive = false);
        // 构造函数,使用地址、键值对key、回调函数和是否递归监控初始化监控对象
        Watcher(std::string const& address, std::string const& key, std::function<void(Response)> callback, bool recursive = false);
        bool Wait();
        bool Wait(std::function<void(bool)> callback);
        bool Cancel(); 
    };
}

六. etcd-cpp-apiv3 使用样例

1. 目录结构

bash 复制代码
etcd/
|-- dir_get.cc
|-- dir_put.cc
|-- makefile
|-- simple_get.cc
|-- simple_put.cc

2. 项目构建

bash 复制代码
# makefile
all: simple_put simple_get dir_get dir_put
simple_put: simple_put.cc
	g++ -o $@ $^ -std=c++17 -lcpprest -letcd-cpp-api
simple_get: simple_get.cc
	g++ -o $@ $^ -std=c++17 -lcpprest -letcd-cpp-api
dir_get: dir_get.cc
	g++ -o $@ $^ -std=c++17 -lcpprest -letcd-cpp-api
dir_put: dir_put.cc
	g++ -o $@ $^ -std=c++17 -lcpprest -letcd-cpp-api

.PHONY: clean
clean:
	rm -f simple_put simple_get dir_get dir_put

3. 代码实现

3.1 简单数据操作

cpp 复制代码
// simple_put.cc
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>

int main() 
{
    // 1.创建 etcd 客户端,连接到 etcd 服务器
    const std::string url = "http://192.168.174.128:2379";
    etcd::Client client(url);
    // 2.创建租约:如果 3 秒内不续约,租约会自动过期,所有关联这个租约的 key 会被删除
    etcd::Response lease_resp = client.leasegrant(3).get();
    if (lease_resp.is_ok() == false) {
        std::cout << "创建租约失败:" << lease_resp.error_message() << std::endl;
        return -1;
    }
    // 3.获取租约 ID
    int64_t lease_id = lease_resp.value().lease();
    // 4.设置键值对并关联租约 ID
    etcd::Response resp = client.put("name", "zhangsan", lease_id).get();
    if (resp.is_ok() == false) {
        std::cout << "添加数据失败:" << resp.error_message() << std::endl;
        return -1;
    }
    // 5.创建异常处理函数:当 KeepAlive 出问题时,会调用这个函数
    auto handler = [](const std::exception_ptr eptr) {
        try {
            if (eptr) {
                std::rethrow_exception(eptr);
            }
        } catch (const std::runtime_error& e) {
            std::cout << "连接失败:" << e.what() << std::endl;
        } catch (const std::out_of_range& e) {
            std::cout << "租约过期:" << e.what() << std::endl;
        }
    };
    // 6.创建保活对象:每隔 3 秒续约一次租约,保活 3 秒
    etcd::KeepAlive keepalive(url, handler, 3, lease_id);
    std::cout << "回车退出租约失效" << std::endl;
    getchar();

    return 0;
}
cpp 复制代码
// simple_get.cc
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>

void callback(etcd::Response resp) {
    if (resp.is_ok() == false) {
        std::cout << "数据监控失败:" << resp.error_message() << std::endl;
        return;
    }
    const std::vector<etcd::Event>& events = resp.events();
    for (auto &e : events) {
        if (e.event_type() == etcd::Event::EventType::PUT) {
            std::cout << e.kv().key() << "数据改变:" << e.prev_kv().as_string() << " -> " << e.kv().as_string() << std::endl;
        } else if (e.event_type() == etcd::Event::EventType::DELETE_) {
            std::cout << e.prev_kv().key() << "数据删除" << std::endl;
        } else {
            std::cout << "无效事件类型" << std::endl;
        }
    }
}

int main() 
{
    // 1.创建 etcd 客户端,连接到 etcd 服务器
    std::string url = "http://192.168.174.128:2379";
    etcd::Client client(url);
    // 2.获取键值对
    etcd::Response resp = client.get("name").get();
    if (resp.is_ok() == false) {
        std::cout << "获取数据失败:" << resp.error_message() << std::endl;
        return -1;
    }
    std::cout << resp.value().key() << ": " << resp.value().as_string() << std::endl;
    // 3.数据监控:当键值对变化时,会触发回调函数
    etcd::Watcher watcher(url, "name", callback, false);
    // 4.阻塞等待数据监控事件
    watcher.Wait();

    return 0;
}

先执行 put 再执行 get:

然后 put 按下回车结束程序,name 三秒后失活:

再次执行 put:

3.2 目录操作

cpp 复制代码
// dir_put.cc
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>

void put(std::shared_ptr<etcd::KeepAlive>& keepalive, const std::string& url, const std::string& key, const std::string& value) {
    // 1.创建 etcd 客户端,连接到 etcd 服务器
    etcd::Client client(url);
    // 2.创建租约:如果 3 秒内不续约,租约会自动过期,所有关联这个租约的 key 会被删除
    etcd::Response lease_resp = client.leasegrant(3).get();
    if (lease_resp.is_ok() == false) {
        std::cout << "创建租约失败:" << lease_resp.error_message() << std::endl;
        return;
    }
    // 3.获取租约 ID
    int64_t lease_id = lease_resp.value().lease();
    // 4.设置键值对并关联租约 ID
    etcd::Response resp = client.put(key, value, lease_id).get();
    if (resp.is_ok() == false) {
        std::cout << "添加数据失败:" << resp.error_message() << std::endl;
        return;
    }
    // 5.创建异常处理函数:当 KeepAlive 出问题时,会调用这个函数
    auto handler = [&](const exception_ptr& eptr) {
        put(keepalive, url, key, value);
    };
    // 6.创建 KeepAlive 实例:用于续约租约,防止租约过期
    keepalive.reset(new etcd::KeepAlive(url, handler, 3, lease_id));
}

int main() 
{
    std::shared_ptr<etcd::KeepAlive> keepalive;
    const std::string url = "http://192.168.174.128:2379";
    const std::string key = "/name/123456";
    const std::string value = "zhangsan";
    put(keepalive, url, key, value);
    std::cout << "回车退出租约失效" << std::endl;
    getchar();

    return 0;
}
cpp 复制代码
// dir_get.cc
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>

void callback(etcd::Response resp) {
    if (resp.is_ok() == false) {
        std::cout << "数据监控失败:" << resp.error_message() << std::endl;
        return;
    }
    const std::vector<etcd::Event>& events = resp.events();
    for (auto &e : events) {
        if (e.event_type() == etcd::Event::EventType::PUT) {
            std::cout << e.kv().key() << "数据改变:" << e.prev_kv().as_string() << " -> " << e.kv().as_string() << std::endl;
        } else if (e.event_type() == etcd::Event::EventType::DELETE_) {
            std::cout << e.prev_kv().key() << "数据删除" << std::endl;
        } else {
            std::cout << "无效事件类型" << std::endl;
        }
    }
}

void get(std::shared_ptr<etcd::Watcher>& watcher, const std::string& url, const std::string& key) {
    // 1.创建 etcd 客户端,连接到 etcd 服务器
    etcd::Client client(url);
    // 2.获取目录下的所有键值对
    etcd::Response resp = client.ls("/name").get();
    if (resp.is_ok() == true) {
        const std::vector<etcd::Value>& values = resp.values();
        for (auto &v : values) {
            std::cout << v.key() << ": " << v.as_string() << std::endl;
        }
    }
    // 3.数据监控:当键值对变化时,会触发回调函数
    watcher.reset(new etcd::Watcher(url, key, callback, true)); // 递归监控目录下的所有键值对
    // 4.阻塞等待数据监控事件
    watcher->Wait([&](bool cond) {
        if (cond == true) { // 当前任务是主动取消的
            return;
        }
        get(watcher, url, key);
    });
}

int main() 
{
    std::shared_ptr<etcd::Watcher> watcher;
    std::string url = "http://192.168.174.128:2379";
    std::string key = "/";
    get(watcher, url, key);
    std::cout << "回车退出监控失效" << std::endl;
    getchar();

    return 0;
}

先执行 put 再执行 get:

然后 put 按下回车结束程序,/name/123456 三秒后失活:

再次执行 put:

七. etcd-cpp-apiv3 封装

1. 设计与实现

封装目的:

  • 将添加数据和数据保活结合在一起,实现服务注册功能。
  • 将获取数据和数据监控结合在一起,实现服务发现功能。

因为 Etcd 是一个内存键值存储中心,且提供了数据改变通知功能,因此作为注册中心也是一个很好的应用。使用 Etcd 作为服务注册发现中心,你需要定义服务的注册和发现逻辑。这通常涉及到以下几个操作:

  1. 服务注册:服务启动时,向 Etcd 注册自己的服务名称和访问地址信息的键值对。
  2. 服务发现:客户端通过 Etcd 获取服务的访问地址信息,用于连接服务器进行远程调用。
  3. 健康检查:服务定期向 Etcd 发送心跳,以维持其注册信息的有效性。

服务管理的封装中,主要需要注意两个点:

  1. 服务注册:向 Etcd 服务器中,添加服务数据;例如:put /user/myid 192.168.174.128:9090
  2. 服务发现:从 Etcd 服务器中,获取服务数据;例如:ls /user

通过从 Etcd 服务器上获取的数据,可以解析获知,当前有哪个主机可以提供 /user 服务

封装思想:

  • 将服务注册和服务发现分别封装类,方便外部能够通过实例化的对象简便实现服务发现/注册。
  • 服务注册类,添加 {/服务名称, 访问地址} 的键值对。
  • 服务发现类,通过 ls 以及监视 / 目录来获取 / 下的所有键值对变动。

服务的注册

封装服务注册类,将客户端请求与租约保活部分封装起来,向外提供一个接口能够实现数据的新增即可,通过实例化的对象可以方便快捷的实现服务注册功能。

服务的发现

封装服务发现类,将客户端请求与路径监视部分封装起来,通过实例化的对象可以方便快捷的实现服务发现功能,并针对发现的服务进行对应处理。

为了能够与其他功能进行解耦,因此这里封装的时候由外部传入针对服务上线和下线所进行处理的接口进行回调处理,当前模块部分本身并不关注具体事件该如何处理。

1.1 目录结构

bash 复制代码
source/
|-- etcd.cc
|-- etcd.h

1.2 代码实现

cpp 复制代码
// etcd.h
#pragma once
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>

namespace xzyetcd {
    // 等待连接注册中心:如果连接失败,会一直重试直到成功
    extern void wait_for_connection(etcd::Client& client);
    // SvcProvider 服务提供类
    class SvcProvider {
    public:
        using ptr = std::shared_ptr<SvcProvider>;
        // key = /_svc_name/instance_id、value = _svc_addr
        SvcProvider(const std::string& reg_center_addr, const std::string& svc_name, const std::string& svc_addr);
        ~SvcProvider();
        // 注册服务:将服务名称和服务地址注册到注册中心
        bool registry();
    private:
        // 生成服务注册键值对的键:/svc_name/instance_id
        std::string make_key();
    private:
        std::string _reg_center_addr; // 注册中心地址,也就是 Etcd 服务器地址:http://192.168.174.128:2379
        std::string _instance_id; // 服务ID:随机生成UUID
        std::string _svc_name; // 节点提供的服务名称
        std::string _svc_addr; // 节点提供的服务地址:192.168.174.128:9000
        std::shared_ptr<etcd::KeepAlive> _keep_alive; // 租约保活对象
    };
    // SvcWatcher 服务监控类
    class SvcWatcher {
    public:
        using ptr = std::shared_ptr<SvcWatcher>;
        using ModCallback = std::function<void(const std::string, const std::string)>;
        SvcWatcher(const std::string& reg_center_addr, ModCallback&& online_callback, ModCallback&& offline_callback);
        ~SvcWatcher();
        // 数据监控:并不针对具体的服务,而是监控 / 目录下的所有服务
        bool watch();
    private:
        // 解析服务键值对的键,获取服务名称:/svc_name/instance_id -> svc_name
        std::string parse_key(const std::string& key);
        // 数据监控回调函数:当服务注册中心的数据发生变化时,会触发该回调函数
        void callback(const etcd::Response& resp);
    private:
        std::string _reg_center_addr; // 注册中心地址,也就是 Etcd 服务器地址
        ModCallback _online_callback; // 服务上线回调函数
        ModCallback _offline_callback; // 服务下线回调函数
        std::shared_ptr<etcd::Watcher> _watcher; // 服务上下线监控对象
    };
}
cpp 复制代码
// etcd.cc
#include <sstream>
#include <thread>
#include "etcd.h"
#include "log.h"
#include "util.h"

namespace xzyetcd {
    // 等待连接注册中心:如果连接失败,会一直重试直到成功
    void wait_for_connection(etcd::Client& client) {
        while (!client.head().get().is_ok()) {
            WRN("连接etcd服务器失败重试中...");
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
    // key = /_svc_name/instance_id、value = _svc_addr
    SvcProvider::SvcProvider(const std::string& reg_center_addr, const std::string& svc_name, const std::string& svc_addr)
        : _reg_center_addr(reg_center_addr), _instance_id(xzyutil::Random::uuid())
        , _svc_name(svc_name), _svc_addr(svc_addr) {}
    SvcProvider::~SvcProvider() {}
    // 生成服务注册键值对的键:/svc_name/instance_id
    std::string SvcProvider::make_key() {
        std::stringstream ss;
        ss << "/" << _svc_name << "/" << _instance_id;
        return ss.str();
    }
    // 注册服务:将服务名称和服务地址注册到注册中心
    bool SvcProvider::registry() {              
        // 1.创建 etcd 客户端,并等待连接到 etcd 服务器
        etcd::Client client(_reg_center_addr);
        wait_for_connection(client);
        // 2.创建3秒租约,并获取租约ID
        etcd::Response lease_resp = client.leasegrant(3).get();
        if (lease_resp.is_ok() == false) {
            ERR("创建租约失败:{}", lease_resp.error_message());
            return false;
        }
        int64_t lease_id = lease_resp.value().lease();
        // 3.重组键值对的键,并向 etcd 服务器添加数据,也就是注册服务
        std::string key = make_key();
        std::string value = _svc_addr;
        etcd::Response resp = client.put(key, value, lease_id).get();
        if (resp.is_ok() == false) {
            ERR("注册服务失败:{}", resp.error_message());
            return false;
        }
        // 4.创建异常处理函数,用于在租约过期时重新执行注册服务
        auto handler = [this](const std::exception_ptr){
            this->registry(); // 递归调用 registry 函数,重新注册服务
        };
        // 5.实例化 KeepAlive 对象,用于保活租约,当租约过期时,会调用异常处理函数
        _keep_alive.reset(new etcd::KeepAlive(_reg_center_addr, handler, 3, lease_id));
        return true;
    }

    SvcWatcher::SvcWatcher(const std::string& reg_center_addr, SvcWatcher::ModCallback&& online_callback, SvcWatcher::ModCallback&& offline_callback)
        : _reg_center_addr(reg_center_addr)
        , _online_callback(std::move(online_callback))
        , _offline_callback(std::move(offline_callback)) {}
    SvcWatcher::~SvcWatcher() {}
    // 解析服务键值对的键,获取服务名称:/svc_name/instance_id -> svc_name
    std::string SvcWatcher::parse_key(const std::string& key) {
        std::vector<std::string> array;
        xzyutil::STR::split(key, "/", array);
        return array[0];
    }
    // 数据监控回调函数:当服务注册中心的数据发生变化时,会触发该回调函数
    void SvcWatcher::callback(const etcd::Response& resp) {
        if (resp.is_ok() == false) {
            ERR("数据监控失败:{}", resp.error_message());
            return;
        }
        const std::vector<etcd::Event>& events = resp.events();
        for (auto &e : events) {
            if (e.event_type() == etcd::Event::EventType::PUT) {
                std::string svc_name = parse_key(e.kv().key()); // 获取服务名称
                std::string svc_addr = e.kv().as_string(); // 获取服务地址
                if (_online_callback) {
                    _online_callback(svc_name, svc_addr); // 调用上线回调函数
                }
            } else if (e.event_type() == etcd::Event::EventType::DELETE_) {
                std::string svc_name = parse_key(e.prev_kv().key()); // 获取服务名称
                std::string svc_addr = e.prev_kv().as_string(); // 获取服务地址
                if (_offline_callback) {
                    _offline_callback(svc_name, svc_addr); // 调用下线回调函数
                }
            } else {
                WRN("无效事件类型");
            }
        }
    }
    // 数据监控:并不针对具体的服务,而是监控 / 目录下的所有服务
    bool SvcWatcher::watch() {
        // 1.创建 etcd 客户端,并等待连接到 etcd 服务器
        etcd::Client client(_reg_center_addr);
        wait_for_connection(client);
        // 2.浏览 / 目录下的所有服务,获取所有当前已提供服务的节点信息
        etcd::Response resp = client.ls("/").get();
        if (resp.is_ok() == true) {
            const std::vector<etcd::Value>& values = resp.values();
            for (auto &v : values) {
                std::string svc_name = parse_key(v.key()); // 获取服务名称
                std::string svc_addr = v.as_string(); // 获取服务地址
                if (_online_callback) {
                    _online_callback(svc_name, svc_addr); // 调用上线回调函数
                }
            }
        }
        // 3.监控 / 目录下的所有服务,当有服务上线或下线时,会触发回调函数
        auto cb = std::bind(&SvcWatcher::callback, this, std::placeholders::_1);
        _watcher.reset(new etcd::Watcher(_reg_center_addr, "/", cb, true));
        // 4.等待监控事件触发,当监控事件触发时,会调用回调函数
        _watcher->Wait([this](bool cond) {
            if (cond == true) {
                return;
            }
            this->watch(); // 递归调用 watch 函数,重新监控 / 目录下的所有服务
        });
        return true;
    }
}

2. 使用样例

2.1 目录结构

bash 复制代码
test/
|-- etcd
    |-- discovery.cc
    |-- makefile
    |-- registry.cc

2.2 项目构建

bash 复制代码
# makefile
all: discovery registry
discovery: discovery.cc ../../source/etcd.cc ../../source/log.cc ../../source/util.cc
	g++ -o $@ $^ -std=c++17 -lfmt -lspdlog -ljsoncpp -lcpprest -letcd-cpp-api
registry: registry.cc ../../source/etcd.cc ../../source/log.cc ../../source/util.cc
	g++ -o $@ $^ -std=c++17 -lfmt -lspdlog -ljsoncpp -lcpprest -letcd-cpp-api

.PHONY: clean
clean:
	rm -f discovery registry

2.3 代码实现

cpp 复制代码
// discovery.cc
#include <iostream>
#include "../../source/etcd.h"
#include "../../source/log.h"

void online(const std::string& svc_name, const std::string& svc_addr) {
    INF("新服务 {} 上线,节点地址:{}", svc_name, svc_addr);
}
void offline(const std::string& svc_name, const std::string& svc_addr) {
    INF("服务 {} 下线,节点地址:{}", svc_name, svc_addr);
}

int main(int argc, char* argv[]) 
{
    xzylog::log_init();
    // 1.实例化服务发现对象
    xzyetcd::SvcWatcher watcher("http://192.168.174.128:2379", online, offline);
    // 2.发现服务
    watcher.watch();
    getchar();

    return 0;
}
cpp 复制代码
// registry.cc
#include <iostream>
#include "../../source/etcd.h"
#include "../../source/log.h"

int main(int argc, char* argv[]) 
{
    xzylog::log_init();
    // 1.实例化服务提供者对象
    xzyetcd::SvcProvider provider("http://192.168.174.128:2379", "user", "192.168.174.128:9000");
    // 2.注册服务
    provider.registry();
    getchar();

    return 0;
}

先执行 discovery,再执行 registry,服务上线:

Ctrl + C 终止 registry,三秒之后服务下线:

八. Etcd 结合 BRpc 使用样例

1. 目录结构

bash 复制代码
test/
|-- etcd_brpc
    |-- cal.proto
    |-- makefile
    |-- rpc_client.cc
    |-- rpc_server.cc

2. 项目构建

bash 复制代码
# makefile
all: rpc_server rpc_client
rpc_server: rpc_server.cc cal.pb.cc ../../source/rpc.cc ../../source/log.cc ../../source/etcd.cc ../../source/util.cc
	g++ -o $@ $^ -std=c++17 -lbrpc -lleveldb -lprotobuf -lpthread -ldl -lssl -lcrypto -lgflags -lfmt -lspdlog -lcpprest -letcd-cpp-api -lpthread -ljsoncpp
rpc_client: rpc_client.cc cal.pb.cc ../../source/rpc.cc ../../source/log.cc ../../source/etcd.cc ../../source/util.cc
	g++ -o $@ $^ -std=c++17 -lbrpc -lleveldb -lprotobuf -lpthread -ldl -lssl -lcrypto -lgflags -lfmt -lspdlog -lcpprest -letcd-cpp-api -lpthread -ljsoncpp

%.pb.cc: %.proto
	protoc --cpp_out=./ $^

.PHONY: clean
clean:
	rm -f rpc_server rpc_client cal.pb.cc cal.pb.h

3. 代码实现

cpp 复制代码
// rpc_server.cc
#include "../../source/rpc.h"
#include "../../source/log.h"
#include "../../source/etcd.h"
#include "../../source/util.h"
#include "cal.pb.h"

// 创建 CalServiceImpl 类来实现 CalService 服务
class CalServiceImpl : public cal::CalService {
public:
    CalServiceImpl() {}
    ~CalServiceImpl() {}
    // 重写 Add 方法
    virtual void Add(::google::protobuf::RpcController* controller,
                    const ::cal::AddReq* request,
                    ::cal::AddRsp* response,
                    ::google::protobuf::Closure* done) override {
        // 当 done_guard 被释放时执行 done->Run() 表明 RPC 同步请求完成
        brpc::ClosureGuard done_guard(done);
        int result = request->num1() + request->num2();
        response->set_result(result);
    }
};

int main(int argc, char* argv[]) 
{
    // 1.初始化日志系统,配置注册中心地址、服务名称和服务地址
    xzylog::log_init();
    std::string reg_center_addr = "http://192.168.174.128:2379";
    std::string reg_svc_name = "CalService";
    std::string reg_svc_addr = "192.168.174.128:9000";
    // 2.创建 CalServiceImpl 实例:用于处理 RPC 请求
    CalServiceImpl* cal_server = new CalServiceImpl();
    // 3.创建 RpcServer 实例:用于监听指定端口并处理 RPC 请求
    std::shared_ptr<brpc::Server> server = xzyrpc::RpcServerFactory::create(9000, cal_server);
    // 4.创建服务提供类实例:用于将服务注册到注册中心
    xzyetcd::SvcProvider provider(reg_center_addr, reg_svc_name, reg_svc_addr);
    // 5.注册服务:将服务名称和服务地址注册到注册中心
    provider.registry();
    // 6.启动 RPC 服务器:开始监听指定端口并处理 RPC 请求
    server->RunUntilAskedToQuit();

    return 0;   
}
cpp 复制代码
// rpc_client.cc
#include "../../source/rpc.h"
#include "../../source/log.h"
#include "../../source/etcd.h"
#include "../../source/util.h"
#include "cal.pb.h"

int main(int argc, char* argv[]) 
{
    // 1.初始化日志系统,配置注册中心地址和服务名称
    xzylog::log_init();
    std::string reg_center_addr = "http://192.168.174.128:2379";
    std::string reg_svc_name = "CalService";
    // 2.创建 ChannelsManager 用于管理服务节点,并关注 CalService 服务
    xzyrpc::ChannelsManager channels_manager;
    channels_manager.setWatch(reg_svc_name);
    // 3.绑定服务节点变更回调函数:用于添加或删除服务节点
    auto online_cb = std::bind(&xzyrpc::ChannelsManager::addNode, &channels_manager, std::placeholders::_1, std::placeholders::_2);
    auto offline_cb = std::bind(&xzyrpc::ChannelsManager::delNode, &channels_manager, std::placeholders::_1, std::placeholders::_2);
    // 4.创建服务发现对象:用于发现 CalService 服务节点的变更
    xzyetcd::SvcWatcher watcher(reg_center_addr, online_cb, offline_cb);
    watcher.watch();
    // 5.等待 CalService 服务节点上线并获取服务节点
    // - 服务节点上线后,会调用 online_cb 回调函数,将服务节点添加到 ChannelsManager 中
    // - 服务节点下线后,会调用 offline_cb 回调函数,将服务节点从 ChannelsManager 中删除
    xzyrpc::ChannelPtr channel = channels_manager.getNode(reg_svc_name);
    while (!channel) {
        ERR("没有可供 CalService 服务节点可供使用");
        std::this_thread::sleep_for(std::chrono::seconds(1));
        channel = channels_manager.getNode(reg_svc_name);
    }
    // 6.创建 Controller、AddReq、AddRsp、Closure 实例:用于发起 RPC 调用
    brpc::Controller* cntl = new brpc::Controller();
    cal::AddReq* req = new cal::AddReq();
    req->set_num1(10);
    req->set_num2(20);
    cal::AddRsp* rsp = new cal::AddRsp();
    google::protobuf::Closure* closure = xzyrpc::ClosureFactory::create([=](){
       std::unique_ptr<brpc::Controller> cntl_guard(cntl);
       std::unique_ptr<cal::AddReq> req_guard(req);
       std::unique_ptr<cal::AddRsp> rsp_guard(rsp);
       if (cntl_guard->Failed() == true) {
           ERR("RPC 调用失败: {}", cntl_guard->ErrorText());
           return;
       }
       std::cout << "RPC 调用结果:" << rsp_guard->result() << std::endl;
    });
    // 7.创建 CalService_Stub 实例,并发起 RPC 调用
    cal::CalService_Stub stub(channel.get());
    stub.Add(cntl, req, rsp, closure);
    std::cout << "====================" << std::endl;
    getchar();

    return 0;   
}

先执行 client,再执行 server,等待服务上线,再执行服务获取结果:

相关推荐
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue球鞋购物系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
仍然.1 小时前
MYSQL--- 表的设计
数据库·mysql
数据知道2 小时前
PostgreSQL的连接方式有哪些?有哪些连接工具?
数据库·postgresql
柚子科技2 小时前
毕业设计不用愁:一个免费的 SQL 转 ER 图在线工具,真香!
数据库·sql·毕业设计·课程设计·毕设
xuefuhe2 小时前
postgresql获取真正的execution plan
数据库·postgresql
xcLeigh2 小时前
KingbaseES数据库:ksql 命令行从建表到删表实战(含避坑指南)
数据库·增删改查·国产数据库·金仓数据库
我是黄骨鱼2 小时前
【零基础学数据库|第五篇】DDL语句的使用
数据库
鸽芷咕2 小时前
从 Query Mapping 到函数缓存,KingbaseES 高级 SQL 调优手段全揭秘
数据库·sql·缓存·金仓数据库
Dxy12393102162 小时前
MySQL的DATETIME字段如何避免隐式转换:索引优化与范围查询实践
数据库·mysql