Skip to content

C++ 连接 etcd

对于 C++ 开发者,官方推荐的客户端库是 etcd-cpp-apiv3,它在 etcd-cpp-api 基础上重写,全面支持了 v3 的 gRPC API[reference:0][reference:1]。

C 语言没有官方支持的客户端库,通常需要使用 C++ 库进行封装或直接调用 gRPC 接口。

下面的教程会以 etcd-cpp-apiv3 库为例,介绍环境搭建、核心操作以及常见的分布式协调功能实现。


📦 一、环境搭建

首先,需要安装 etcd-cpp-apiv3 客户端库。

1. 依赖安装 (以 Ubuntu 为例)

bash
# 1. 安装基础依赖
sudo apt-get install libboost-all-dev libssl-dev

# 2. 安装 gRPC 和 Protobuf
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc libgrpc-dev libgrpc++-dev

# 3. 安装 cpprestsdk (用于异步处理)
sudo apt-get install libcpprest-dev

[reference:2]

2. 编译并安装 etcd-cpp-apiv3

bash
# 1. 克隆项目仓库
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
cd etcd-cpp-apiv3

# 2. 创建并进入构建目录
mkdir build && cd build

# 3. 使用 CMake 进行配置和构建
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc)

# 4. 安装到系统中
sudo make install

[reference:3]

编译时链接库:g++ -std=c++11 main.cpp -letcd-cpp-apiv3 -o main[reference:4]。


🚀 二、快速入门:同步操作

下面的代码演示了最基础的同步 CRUD 操作。

cpp
#include <etcd/Client.hpp>
#include <etcd/Response.hpp>
#include <iostream>

int main() {
    // 1. 初始化客户端,连接到本地 etcd 服务
    etcd::Client client("http://127.0.0.1:2379");

    // 2. 写入键值对 (PUT)
    etcd::Response put_resp = client.set("/test/key", "hello from cpp").get();
    if (put_resp.is_ok()) {
        std::cout << "成功写入键值对" << std::endl;
    } else {
        std::cerr << "写入失败: " << put_resp.error_message() << std::endl;
    }

    // 3. 读取键值对 (GET)
    etcd::Response get_resp = client.get("/test/key").get();
    if (get_resp.is_ok()) {
        // 使用 .value().as_string() 获取键对应的值
        std::cout << "读取到的值: " << get_resp.value().as_string() << std::endl;
    } else {
        std::cerr << "读取失败: " << get_resp.error_message() << std::endl;
    }

    return 0;
}

[reference:5]


🛠️ 三、核心 API 详解

etcd-cpp-apiv3 的所有操作几乎都返回 pplx::task<Response>,这是一个异步任务对象[reference:6]。你可以使用 .get() 方法阻塞等待结果,实现同步调用,也可以使用 .then() 方法设置回调函数来异步处理。

1. CRUD 操作

操作类型函数签名与示例
写入/修改client.set(key, value, ttl=0)
示例:client.set("/mykey", "myvalue")[reference:7]
读取client.get(key)
示例:client.get("/mykey")[reference:8]
前缀读取client.ls(key_prefix)
示例:client.ls("/services/") 获取该目录下所有子键[reference:9]
删除client.rm(key)
示例:client.rm("/mykey")

注意:删除操作与 v2 API 略有不同,v3 API 使用 rm 方法。

2. 异步与同步处理

异步非阻塞模式:

cpp
// 发起异步读取,设置回调函数
client.get("/some/key").then([](etcd::Response resp) {
    if (resp.is_ok()) {
        std::cout << "异步获取的值: " << resp.value().as_string() << std::endl;
    }
});
// 主线程可以继续执行其他任务,不会阻塞

[reference:10]


⏳ 四、高级特性:租约、监听与分布式锁

1. 租约 (Lease) - 自动过期

通过 lease() 方法创建一个带有存活时间 (TTL) 的租约,并用于写入操作。

cpp
#include <etcd/Client.hpp>
#include <etcd/Lease.hpp>
#include <thread>

int main() {
    etcd::Client client("http://127.0.0.1:2379");
    
    // 创建一个 TTL 为 5 秒的租约
    etcd::Lease lease = client.lease(5).get();
    
    // 将键绑定到该租约上
    client.set("/temp/data", "this will expire", lease.lease_id()).get();
    std::cout << "键已写入,将在5秒后过期" << std::endl;

    // 模拟等待6秒,让租约过期
    std::this_thread::sleep_for(std::chrono::seconds(6));
    
    // 尝试读取,应该失败
    auto resp = client.get("/temp/data").get();
    if (!resp.is_ok()) {
        std::cout << "键已自动过期删除" << std::endl;
    }
    
    return 0;
}

[reference:11][reference:12]

2. 监听器 (Watcher) - 实时感知变化

Watcher 类用于监听一个或多个键的变化,并在回调函数中处理这些事件。

cpp
#include <etcd/Client.hpp>
#include <etcd/Watcher.hpp>
#include <iostream>

int main() {
    etcd::Client client("http://127.0.0.1:2379");
    
    // 定义一个回调函数,用于处理收到的事件
    auto callback = [](etcd::Response resp) {
        if (resp.is_ok()) {
            for (const auto& event : resp.events()) {
                std::cout << "事件类型: " << event.event_type() 
                          << " 键: " << event.kv().key() 
                          << " 值: " << event.kv().as_string() << std::endl;
            }
        }
    };
    
    // 创建一个 Watcher 对象,监听 "/watch/key" 这个键
    etcd::Watcher watcher(client, "/watch/key", callback);
    
    // 保持程序运行,以持续接收事件
    std::cout << "正在监听,请在其他终端执行修改操作..." << std::endl;
    std::this_thread::sleep_for(std::chrono::minutes(1));
    
    return 0;
}

[reference:13]

3. 分布式锁 (Lock) - 互斥与协调

Lock 类提供了一种简单的方式来实现分布式互斥锁,确保同一时间只有一个客户端持有锁。

cpp
#include <etcd/Client.hpp>
#include <etcd/Lock.hpp>

int main() {
    etcd::Client client("http://127.0.0.1:2379");
    
    // 1. 创建一个锁对象,并指定要锁定的资源名称
    etcd::Lock lock(client, "/resource/lock_name");
    
    // 2. 尝试获取锁
    if (lock.lock().get().is_ok()) {
        std::cout << "成功获取分布式锁" << std::endl;
        // 执行需要互斥访问的代码...
        
        // 3. 释放锁
        lock.unlock().get();
        std::cout << "锁已释放" << std::endl;
    } else {
        std::cout << "获取锁失败" << std::endl;
    }
    
    return 0;
}

[reference:14]


📝 五、综合示例:服务注册与发现

这是一个结合了租约和监听器功能的完整服务注册与发现示例,实际应用中可以参考该示例进行封装[reference:15]。

cpp
#include <etcd/Client.hpp>
#include <etcd/Lease.hpp>
#include <etcd/Watcher.hpp>
#include <iostream>
#include <thread>

class ServiceRegistry {
private:
    etcd::Client client_;
    int64_t lease_id_;
    std::string key_;
    std::unique_ptr<etcd::Watcher> watcher_;
    
public:
    ServiceRegistry(const std::string& etcd_url, const std::string& key)
        : client_(etcd_url), key_(key) {}
    
    // 注册服务,租约自动续期
    void register_service(int ttl_seconds) {
        // 1. 创建租约
        auto lease_resp = client_.lease(ttl_seconds).get();
        lease_id_ = lease_resp.value().lease_id();
        
        // 2. 写入服务地址并绑定租约
        client_.set(key_, "http://127.0.0.1:8080", lease_id_).get();
        
        // 3. 启动一个线程,定期刷新租约
        std::thread([this, ttl_seconds]() {
            while (true) {
                std::this_thread::sleep_for(std::chrono::seconds(ttl_seconds / 2));
                client_.refresh_lease(lease_id_).get();
            }
        }).detach();
    }
    
    // 监听服务变更
    void watch_service(const std::string& prefix) {
        watcher_ = std::make_unique<etcd::Watcher>(
            client_, prefix, [](etcd::Response resp) {
                for (const auto& ev : resp.events()) {
                    std::cout << "服务列表变更: " << ev.kv().key() << std::endl;
                }
            }, true);
    }
};

int main() {
    ServiceRegistry registry("http://127.0.0.1:2379", "/services/api");
    registry.register_service(5);
    registry.watch_service("/services/");
    
    std::cout << "服务注册成功,正在运行..." << std::endl;
    std::this_thread::sleep_for(std::chrono::minutes(1));
    
    return 0;
}

⚠️ 六、关键注意事项

  1. 线程安全:该库是线程安全的,可以在多个线程中共享 etcd::Client 对象。
  2. 错误处理:务必检查 Response.is_ok(),处理网络错误或 etcd 返回的错误[reference:16]。
  3. 连接池etcd::Client 内部维护了连接池,通常无需自行管理[reference:17]。
  4. API 兼容性:确保 etcd-cpp-apiv3 库的版本与 etcd 服务器版本兼容,推荐使用较新版本。
  5. 配置连接:客户端构造时支持指定多个集群地址(逗号分隔)并设置负载均衡策略,如 round_robin(默认)、pick_first 等[reference:18]。

通过以上介绍,你应该能在 C++ 项目中快速上手使用 etcd 了。etcd-cpp-apiv3 库的 examples/ 目录下还有更多完整示例可供参考[reference:19]。如果需要更深入的实践,建议查阅其官方文档。