Linux系统C++开发环境搭建工具(二)—— etcd 使用指南

文章目录

etcd简介

etcd 是一个分布式、高可用的键值存储系统(以key: val的形式做数据存储),主要用于在分布式系统中安全地存储和管理关键数据。 它最著名的角色是 Kubernetes 的"大脑",负责存储整个集群的状态和配置信息。

etcd 服务器类似于一个数据库,存储键值对数据。所有客户端都可以通过长连接共享这些数据。一个客户端修改了键值对,etcd 服务器会通知所有正在监听该键值对的客户端。

常用场景:服务发现(Service Discovery)

  • 场景:在微服务架构中,服务实例的 IP 和端口是动态变化的。服务启动时可以将自己的地址注册到 etcd,消费者则从 etcd 查询可用的服务地址。
  • 类比:就像电话簿,服务在这里"登记"和"查找"。

图示:

etcd安装与使用

安装:

shell 复制代码
sudo apt-get install etcd

配置文件:

shell 复制代码
vim /etc/default/etcd

更改绑定监听地址,使用外部网络能访问:
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0"

更改外部访问的地址:
ETCD_ADVERTISE_CLIENT_URLS="http://公网ip:端口"

重新启动:

shell 复制代码
sudo systemctl restart etcd

配置指定API版本:

shell 复制代码
vim /etc/profile

在末尾加:
export ETCDCTL_API=3

重新加载配置文件:

shell 复制代码
source /etc/profile

验证:

搭建c++客户端,使用第三方库API接口

下载库:

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库源码(可选):

shell 复制代码
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
cd etcd-cpp-apiv3
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc) && sudo make install

API接口的使用

  • Client对象:客户端操作句柄对象
    在构建该对象时需要传入参数:url_or_endpoints,即字符串或字符串向量,指定 etcd 服务地址。键值数据的推送和拉取,租约的设置等都是通过该对象提供的接口完成。
  • KeepAlive保活对象:针对一个租约可以不断进行续租,从而维持租约数据的有效性。
    该对象通过Client对象提供的leasekeepalive()租约接口返回值再进行get()而得到。
  • Response对象:针对请求进行响应。
    该对象通常由Client对象提供的接口put()/ls()返回而得到。
  • Value对象:存放键值对数据。
  • Watcher对象:进行数据变化通知。
    构建该对象需要参数:
    • client对象
    • 要监听的键名
    • 变化回调函数
    • 是否递归监听前缀匹配的所有键,通常填true

图示:

示例:

put.cc

shell 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <thread>
int main(int argc,char* argv[])
{
    //创建客户端
    etcd::Client client("http://127.0.0.1:2379");
    //指定租约并获取租约保活对象
    auto keep_alive = client.leasekeepalive(3).get();
    //获取租约id
    auto lease_id = keep_alive->Lease();
    //向etcd新增数据
    auto rsp = client.put("/source/user","127.0.0.1:6000",lease_id).get();
    if(rsp.is_ok()==false)
        std::cout<<"新增数据失败"<<std::endl;
    rsp = client.put("/source/file","127.0.0.1:6001",lease_id).get();
    if(rsp.is_ok()==false)
        std::cout<<"新增数据失败"<<std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 0;
}

get.cc

shell 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <etcd/SyncClient.hpp>

void callback(const etcd::Response& rsp)
{
    if(rsp.is_ok()==false)
    {
        std::cout<<"错误事件通知"<<rsp.error_message()<<std::endl;
        return;
    }
    for(auto const& ev: rsp.events())
    {
        if (ev.event_type() == etcd::Event::EventType::PUT) {
            std::cout << "服务信息发生了改变:\n" ;
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
            std::cout << "服务信息下线被删除:\n";
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }
    }
}

int main(int argc,char* argv[])
{
    //创建客户端
    etcd::Client client("http://127.0.0.1:2379");
    //获取数据
    auto rsp = client.ls("/source").get();
    if(rsp.is_ok()==false)
    {
        std::cout<<"数据获取失败"<<std::endl;
        return -1;
    }
    int n = rsp.keys().size();
    for(int i = 0;i<n;i++)
    {
        std::cout<<rsp.value(i).as_string()<<":"<<rsp.key(i)<<std::endl;
    }
    auto watcher = etcd::Watcher(client,"/source",callback,true);
    watcher.Wait();
    return 0;
}

Makefile:

Makefile 复制代码
all:put get
put:put.cc
	g++ -o $@ $^ -letcd-cpp-api -lcpprest
get:get.cc
	g++ -o $@ $^ -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
	rm -rf put get

以服务发现场景为例进行二次封装

使用原生 API 接口较为复杂繁琐,接下来我们针对服务发现场景进行二次封装

两个客户端:

  • 服务注册客户端:向服务器新增服务信息数据,并进行保活
  • 服务发现客户端:从服务器查找服务信息数据,并进行改变事件监控

思想:

  1. 封装服务注册客户端类:
    提供一个接口:向服务器新增数据并保活
    参数:注册中心地址、新增的服务信息
  2. 封装服务发现客户端类:
    提供两个设置回调函数的接口:提供服务上线和下线的事件处理接口
    提供一个设置根目录的接口:用于获取指定目录下的数据以及监控目录下的数据的改变

示例:

shell 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <etcd/SyncClient.hpp>
#include <memory>
#include <functional>
class Registry
{
public:
    Registry(const std::string& host)
    :_client(std::make_shared<etcd::Client>(host))
    ,_keep_alive(_client->leasekeepalive(3).get())
    ,_lease_id(_keep_alive->Lease())
    {}
    bool registry(const std::string& key,const std::string& val)
    {
        auto rsp = _client->put(key,val,_lease_id,true).get();
        if(rsp.is_ok()==false)
        {
            //建议替换为日志输出
            std::cout<<key<<": "<<val<<"注册失败"<<std::endl;
            return false;
        }
        else return true;
    }
    ~Registry()
    {
        _client->Cancel();
    }
private:
    std::shared_ptr<etcd::Client> _client;
    std::shared_ptr<etcd::KeepAlive> _keep_alive;
    uint64_t _lease_id;
};
class Discovery
{
public:
    using NotifyCallback = std::function<void(std::string,std::string)>;
    Discovery(const std::string& host
        ,const std::string& basedir
        ,const NotifyCallback& put_cb
        ,const NotifyCallback& del_cb)
    :
    _client(std::make_shared<etcd::Client>(host))
    ,_put_cb(put_cb)
    ,_del_cb(del_cb)
    {
        auto rsp = _client->ls(basedir).get();
        if(rsp.is_ok()==false)
        {
            std::cout<<"获取数据失败 "<<rsp.error_message()<<std::endl;
        }
        int sz = rsp.keys().size();
        for(int i=0;i<sz;i++)
        {
            if(_del_cb)
                _put_cb(rsp.key(i),rsp.value(i).as_string());
        }
        _watcher = std::make_shared<etcd::Watcher>(*_client,basedir
            ,std::bind(&Discovery::callback,this, std::placeholders::_1),true);
    }
    void callback(const etcd::Response& rsp)
    {
        if(rsp.is_ok()==false)
        {
            std::cout<<"错误事件通知"<<rsp.error_message()<<std::endl;
            return;
        }
        for(auto const& ev: rsp.events())
        {
            if (ev.event_type() == etcd::Event::EventType::PUT) {
                std::cout << "上线服务:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            }else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
                std::cout << "下线服务:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            }
        }
    }
    ~Discovery()
    {
        _watcher->Cancel();
    }
private:
    NotifyCallback _put_cb;
    NotifyCallback _del_cb;
    std::shared_ptr<etcd::Client> _client;
    std::shared_ptr<etcd::Watcher> _watcher;
};

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!

相关推荐
励志不掉头发的内向程序员3 小时前
【Linux系列】解码 Linux 内存地图:从虚拟到物理的寻宝之旅
linux·运维·服务器·开发语言·学习
woshihonghonga4 小时前
停止Conda开机自动运行方法
linux·人工智能·conda
深盾科技4 小时前
C/C++逆向分析实战:变量的奥秘与安全防护
c语言·c++·安全
ajassi20007 小时前
开源 C++ QT QML 开发(二十)多媒体--摄像头拍照
c++·qt·开源
_OP_CHEN7 小时前
C++基础:(十二)list类的基础使用
开发语言·数据结构·c++·stl·list类·list核心接口·list底层原理
遇见火星7 小时前
Ubuntu Docker 容器化部署教程
linux·ubuntu·docker
ybb_ymm9 小时前
mysql8在linux下的默认规则修改
linux·运维·数据库·mysql
半梦半醒*9 小时前
zabbix安装
linux·运维·前端·网络·zabbix
晚风残10 小时前
【C++ Primer】第六章:函数
开发语言·c++·算法·c++ primer