feat: 新增 C++ 和 Rust 客户端 SDK 及 Docker 构建支持

- 新增 kenaito-conf-cpp-client / kenaito-conf-rust-client 及对应 demo
- 新增 Dockerfile.cpp / Dockerfile.rust
- README 目录结构补充 C++/Rust 客户端
- README_DOCKERFILE 补充 Windows docker run 命令,新增 C++/Rust 段落
This commit is contained in:
2026-08-11 21:14:04 +08:00
parent 436971d154
commit f63bcfa78b
19 changed files with 26281 additions and 12 deletions
+45
View File
@@ -0,0 +1,45 @@
cmake_minimum_required(VERSION 3.10)
project(kenaito-conf-cpp-client VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(CURL REQUIRED)
# 自动下载 nlohmann/json(单头文件)
set(JSON_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/nlohmann)
if(NOT EXISTS ${JSON_INCLUDE_DIR}/json.hpp)
message(STATUS "Downloading nlohmann/json.hpp...")
file(MAKE_DIRECTORY ${JSON_INCLUDE_DIR})
file(DOWNLOAD
https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp
${JSON_INCLUDE_DIR}/json.hpp
SHOW_PROGRESS
)
endif()
add_library(kenaito_conf_client STATIC
src/client.cpp
)
target_include_directories(kenaito_conf_client
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party
PRIVATE
${CURL_INCLUDE_DIRS}
)
target_link_libraries(kenaito_conf_client
PRIVATE
${CURL_LIBRARIES}
)
# Install
install(TARGETS kenaito_conf_client
ARCHIVE DESTINATION lib
)
install(DIRECTORY include/kenaito_conf
DESTINATION include
)
+112
View File
@@ -0,0 +1,112 @@
# Kenaito Config Center C++ Client
C++ 客户端 SDK,适用于 C++11 及以上项目。
## 功能特性
- 从配置中心拉取应用配置
- 定时同步配置变更
- 支持配置变更回调监听
- 本地文件缓存(配置中心不可用时降级)
## 依赖
- C++11 或更高版本
- libcurl
- nlohmann/json(已包含在 third_party 目录)
## 编译
```bash
cd kenaito-conf-cpp-client
mkdir build && cd build
cmake ..
make
```
## 使用
```cpp
#include <kenaito_conf/client.h>
#include <iostream>
int main() {
kenaito::Config config;
config.base_url = "http://localhost:8080";
config.app_name = "my-app";
config.env = "daily";
config.sync_interval = 10;
config.cache_dir = ".";
kenaito::Client client(config);
// 监听配置变更
client.on_change([](const std::map<std::string, std::string>& changed) {
std::cout << "Config changed:" << std::endl;
for (const auto& [k, v] : changed) {
std::cout << " " << k << " = " << v << std::endl;
}
});
// 启动客户端
if (!client.start()) {
std::cerr << "Failed to start config client" << std::endl;
return 1;
}
// 获取配置
std::string db_host = client.get("database.host");
std::cout << "database.host = " << db_host << std::endl;
// 获取所有配置
auto all = client.get_all();
for (const auto& [k, v] : all) {
std::cout << k << " = " << v << std::endl;
}
// 手动同步
client.sync_once();
// 停止
client.stop();
return 0;
}
```
## 配置项
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `base_url` | string | 配置中心地址 | `http://localhost:8080` |
| `app_name` | string | 应用名称 | (必填) |
| `env` | string | 环境标识 | `dev` |
| `version` | string | 配置版本 | `latest` |
| `token` | string | 应用 Token | 空 |
| `sync_interval` | int | 同步间隔(秒) | `60` |
| `ignore_global` | bool | 忽略全局配置 | `false` |
| `cache_dir` | string | 缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-cpp-client-demo
mkdir build && cd build
cmake ..
make
./demo
```
## 环境变量
Demo 支持以下环境变量:
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `KENAITO_BASE_URL` | 配置中心地址 | `http://localhost:8080` |
| `KENAITO_APP_NAME` | 应用名称 | `kenaito-springboot-demo` |
| `KENAITO_ENV` | 环境标识 | `daily` |
| `KENAITO_VERSION` | 配置版本 | `latest` |
| `KENAITO_TOKEN` | 应用 Token | 空 |
| `KENAITO_SYNC_INTERVAL` | 同步间隔(秒) | `10` |
| `KENAITO_CACHE_DIR` | 缓存目录 | `.` |
+112
View File
@@ -0,0 +1,112 @@
# Kenaito Config Center C++ Client
C++ client SDK for the Kenaito Config Center, suitable for C++11 and above.
## Features
- Fetch application configs from the config center
- Periodic config sync
- Config change callback listener
- Local file cache (fallback when config center is unavailable)
## Dependencies
- C++11 or higher
- libcurl
- nlohmann/json (included in third_party directory)
## Build
```bash
cd kenaito-conf-cpp-client
mkdir build && cd build
cmake ..
make
```
## Usage
```cpp
#include <kenaito_conf/client.h>
#include <iostream>
int main() {
kenaito::Config config;
config.base_url = "http://localhost:8080";
config.app_name = "my-app";
config.env = "daily";
config.sync_interval = 10;
config.cache_dir = ".";
kenaito::Client client(config);
// Listen for config changes
client.on_change([](const std::map<std::string, std::string>& changed) {
std::cout << "Config changed:" << std::endl;
for (const auto& [k, v] : changed) {
std::cout << " " << k << " = " << v << std::endl;
}
});
// Start client
if (!client.start()) {
std::cerr << "Failed to start config client" << std::endl;
return 1;
}
// Get config
std::string db_host = client.get("database.host");
std::cout << "database.host = " << db_host << std::endl;
// Get all configs
auto all = client.get_all();
for (const auto& [k, v] : all) {
std::cout << k << " = " << v << std::endl;
}
// Manual sync
client.sync_once();
// Stop
client.stop();
return 0;
}
```
## Configuration
| Parameter | Type | Description | Default |
|-----------|------|-------------|---------|
| `base_url` | string | Config center URL | `http://localhost:8080` |
| `app_name` | string | Application name | (Required) |
| `env` | string | Environment | `dev` |
| `version` | string | Config version | `latest` |
| `token` | string | App token | Empty |
| `sync_interval` | int | Sync interval (seconds) | `60` |
| `ignore_global` | bool | Whether to ignore global configs | `false` |
| `cache_dir` | string | Cache directory | `.` |
## Demo
```bash
cd kenaito-conf-cpp-client-demo
mkdir build && cd build
cmake ..
make
./demo
```
## Environment Variables
The demo supports the following environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `KENAITO_BASE_URL` | Config center URL | `http://localhost:8080` |
| `KENAITO_APP_NAME` | Application name | `kenaito-springboot-demo` |
| `KENAITO_ENV` | Environment | `daily` |
| `KENAITO_VERSION` | Config version | `latest` |
| `KENAITO_TOKEN` | App token | Empty |
| `KENAITO_SYNC_INTERVAL` | Sync interval (seconds) | `10` |
| `KENAITO_CACHE_DIR` | Cache directory | `.` |
@@ -0,0 +1,77 @@
#pragma once
#include <string>
#include <map>
#include <vector>
#include <functional>
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
namespace kenaito {
struct Config {
std::string base_url = "http://localhost:8080";
std::string app_name;
std::string env = "dev";
std::string version = "latest";
std::string token;
int sync_interval = 60;
bool ignore_global = false;
std::string cache_dir = ".";
};
using ChangeHandler = std::function<void(const std::map<std::string, std::string>&)>;
class Cache {
public:
explicit Cache(const std::string& dir);
bool write(const std::string& json_str);
std::string read();
private:
std::string dir_;
static const std::string CACHE_FILE;
};
class Listener {
public:
void on_change(ChangeHandler handler);
void notify(const std::map<std::string, std::string>& changed);
private:
std::mutex mu_;
std::vector<ChangeHandler> handlers_;
};
class Client {
public:
explicit Client(const Config& config);
~Client();
void on_change(ChangeHandler handler);
std::string get(const std::string& key);
std::map<std::string, std::string> get_all();
bool start();
void stop();
void sync_once();
private:
bool fetch_all();
bool fetch_changed();
void apply_configs(const std::map<std::string, std::string>& data);
std::string http_post(const std::string& url, const std::string& body);
Config config_;
Cache cache_;
Listener listener_;
std::map<std::string, std::string> configs_;
int64_t last_time_ = 0;
std::mutex mu_;
std::atomic<bool> started_{false};
std::atomic<bool> stop_flag_{false};
std::thread sync_thread_;
};
} // namespace kenaito
+293
View File
@@ -0,0 +1,293 @@
#include "kenaito_conf/client.h"
#include <curl/curl.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include <algorithm>
// nlohmann/json single header is bundled
#include "nlohmann/json.hpp"
using json = nlohmann::json;
namespace kenaito {
// ---- Cache ----
const std::string Cache::CACHE_FILE = "config-cache.json";
Cache::Cache(const std::string& dir) : dir_(dir) {}
bool Cache::write(const std::string& json_str) {
std::string path = dir_ + "/" + CACHE_FILE;
std::ofstream ofs(path);
if (!ofs.is_open()) return false;
ofs << json_str;
return true;
}
std::string Cache::read() {
std::string path = dir_ + "/" + CACHE_FILE;
std::ifstream ifs(path);
if (!ifs.is_open()) return "";
std::stringstream ss;
ss << ifs.rdbuf();
return ss.str();
}
// ---- Listener ----
void Listener::on_change(ChangeHandler handler) {
std::lock_guard<std::mutex> lock(mu_);
handlers_.push_back(std::move(handler));
}
void Listener::notify(const std::map<std::string, std::string>& changed) {
std::lock_guard<std::mutex> lock(mu_);
for (auto& h : handlers_) {
h(changed);
}
}
// ---- Client ----
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
auto* str = static_cast<std::string*>(userp);
str->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
Client::Client(const Config& config)
: config_(config), cache_(config.cache_dir) {
if (config_.version.empty()) config_.version = "latest";
if (config_.sync_interval <= 0) config_.sync_interval = 60;
if (config_.cache_dir.empty()) config_.cache_dir = ".";
}
Client::~Client() {
stop();
}
void Client::on_change(ChangeHandler handler) {
listener_.on_change(std::move(handler));
}
std::string Client::get(const std::string& key) {
std::lock_guard<std::mutex> lock(mu_);
auto it = configs_.find(key);
return it != configs_.end() ? it->second : "";
}
std::map<std::string, std::string> Client::get_all() {
std::lock_guard<std::mutex> lock(mu_);
return configs_;
}
bool Client::start() {
if (started_.exchange(true)) return true;
if (!fetch_all()) {
std::cerr << "[ConfigClient] fetch all configs error, trying cache" << std::endl;
std::string cached = cache_.read();
if (!cached.empty()) {
try {
auto j = json::parse(cached);
std::map<std::string, std::string> data;
for (auto& [k, v] : j.items()) {
if (v.is_string()) data[k] = v.get<std::string>();
}
apply_configs(data);
std::cerr << "[ConfigClient] loaded " << configs_.size() << " configs from cache" << std::endl;
} catch (const std::exception& e) {
std::cerr << "[ConfigClient] cache parse error: " << e.what() << std::endl;
return false;
}
} else {
std::cerr << "[ConfigClient] fetch configs failed and no cache" << std::endl;
return false;
}
}
return true;
}
void Client::stop() {
stop_flag_ = true;
if (sync_thread_.joinable()) {
sync_thread_.join();
}
}
void Client::sync_once() {
if (!fetch_changed()) {
std::cerr << "[ConfigClient] sync error" << std::endl;
}
}
std::string Client::http_post(const std::string& url, const std::string& body) {
CURL* curl = curl_easy_init();
if (!curl) return "";
std::string response;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) return "";
return response;
}
bool Client::fetch_all() {
std::string url = config_.base_url + "/api/fetchAllConfigs";
json payload;
payload["appName"] = config_.app_name;
payload["env"] = config_.env;
payload["version"] = config_.version;
payload["ignoreGlobal"] = config_.ignore_global;
payload["token"] = config_.token;
std::string resp = http_post(url, payload.dump());
if (resp.empty()) return false;
try {
auto j = json::parse(resp);
if (j.contains("code") && j["code"].get<int>() != 0) {
std::string msg = j.value("msg", "unknown");
std::cerr << "[ConfigClient] server error: " << msg << std::endl;
return false;
}
if (j.contains("latest.fetch.timestamp")) {
last_time_ = std::stoll(j["latest.fetch.timestamp"].get<std::string>());
}
// cache raw response
cache_.write(resp);
// apply configs
std::map<std::string, std::string> data;
for (auto& [k, v] : j.items()) {
if (v.is_string()) data[k] = v.get<std::string>();
}
apply_configs(data);
std::cerr << "[ConfigClient] fetched " << configs_.size() << " configs" << std::endl;
return true;
} catch (const std::exception& e) {
std::cerr << "[ConfigClient] parse error: " << e.what() << std::endl;
return false;
}
}
bool Client::fetch_changed() {
std::string url = config_.base_url + "/api/fetchChangedConfigs";
json payload;
payload["appName"] = config_.app_name;
payload["env"] = config_.env;
payload["version"] = config_.version;
payload["lastTime"] = last_time_;
payload["ignoreGlobal"] = config_.ignore_global;
std::string resp = http_post(url, payload.dump());
if (resp.empty()) {
std::cerr << "[ConfigClient] fetch_changed: empty response (network error or timeout)" << std::endl;
return false;
}
try {
auto j = json::parse(resp);
if (j.contains("code") && j["code"].get<int>() != 0) {
std::string msg = j.value("msg", "unknown");
std::cerr << "[ConfigClient] fetch_changed: server error code=" << j["code"] << " msg=" << msg << std::endl;
return false;
}
if (!j.contains("data")) {
std::cerr << "[ConfigClient] fetch_changed: missing 'data' field in response" << std::endl;
return false;
}
auto data = j["data"];
if (data.contains("latest.fetch.timestamp")) {
last_time_ = std::stoll(data["latest.fetch.timestamp"].get<std::string>());
data.erase("latest.fetch.timestamp");
}
// handle deleted keys
if (data.contains("deletedKeys") && !data["deletedKeys"].get<std::string>().empty()) {
std::string deleted_str = data["deletedKeys"].get<std::string>();
std::istringstream iss(deleted_str);
std::string key;
std::vector<std::string> deleted_keys;
while (std::getline(iss, key, ',')) {
std::lock_guard<std::mutex> lock(mu_);
configs_.erase(key);
deleted_keys.push_back(key);
}
std::string keys;
for (const auto& k : deleted_keys) {
if (!keys.empty()) keys += ", ";
keys += k;
}
std::cerr << "[ConfigClient] " << deleted_keys.size() << " configs deleted: " << keys << std::endl;
data.erase("deletedKeys");
}
// handle changed configs
std::map<std::string, std::string> changed;
for (auto& [k, v] : data.items()) {
if (v.is_string()) {
changed[k] = v.get<std::string>();
}
}
if (!changed.empty()) {
{
std::lock_guard<std::mutex> lock(mu_);
for (auto& [k, v] : changed) {
configs_[k] = v;
}
}
listener_.notify(changed);
std::string keys;
for (auto& [k, v] : changed) {
if (!keys.empty()) keys += ", ";
keys += k;
}
std::cerr << "[ConfigClient] " << changed.size() << " configs changed: " << keys << std::endl;
}
return true;
} catch (const std::exception& e) {
std::cerr << "[ConfigClient] fetch_changed: parse error: " << e.what() << std::endl;
std::cerr << "[ConfigClient] response was: " << resp.substr(0, 200) << std::endl;
return false;
}
}
void Client::apply_configs(const std::map<std::string, std::string>& data) {
std::lock_guard<std::mutex> lock(mu_);
for (auto& [k, v] : data) {
if (k.substr(0, 8) == "kenaito." || k == "latest.fetch.timestamp") continue;
configs_[k] = v;
}
}
} // namespace kenaito
File diff suppressed because it is too large Load Diff