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
+36
View File
@@ -0,0 +1,36 @@
# Build stage
FROM alpine:3.20 AS build
WORKDIR /src
# Install build dependencies
RUN apk add --no-cache \
build-base \
cmake \
curl-dev \
git
# Copy client library
COPY kenaito-conf-cpp-client/ ./kenaito-conf-cpp-client/
# Copy demo project
COPY kenaito-conf-cpp-client-demo/ ./kenaito-conf-cpp-client-demo/
# Build
WORKDIR /src/kenaito-conf-cpp-client-demo
RUN mkdir build && cd build && \
cmake .. && \
make
# Runtime stage
FROM alpine:3.20
WORKDIR /app
# Install runtime dependencies
RUN apk add --no-cache libcurl libstdc++ libgcc
# Copy binary
COPY --from=build /src/kenaito-conf-cpp-client-demo/build/demo /app/demo
ENTRYPOINT ["./demo"]
+29
View File
@@ -0,0 +1,29 @@
# Build stage
FROM rust:1.88 AS build
WORKDIR /src
# Copy client library
COPY kenaito-conf-rust-client/ ./kenaito-conf-rust-client/
# Copy demo project
COPY kenaito-conf-rust-client-demo/ ./kenaito-conf-rust-client-demo/
# Build
WORKDIR /src/kenaito-conf-rust-client-demo
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy binary
COPY --from=build /src/kenaito-conf-rust-client-demo/target/release/kenaito-conf-rust-client-demo /app/demo
ENTRYPOINT ["./demo"]
+5 -1
View File
@@ -31,7 +31,11 @@ kenaito-conf/
├── kenaito-conf-dotnet-client/ # .NET 客户端 SDK(.NET 6.0+)
├── kenaito-conf-dotnet-client-demo/ # .NET 客户端示例
├── kenaito-conf-lua-client/ # Lua 客户端 SDK(Lua 5.1+)
└── kenaito-conf-lua-client-demo/ # Lua 客户端示例
├── kenaito-conf-lua-client-demo/ # Lua 客户端示例
├── kenaito-conf-cpp-client/ # C++ 客户端 SDK(C++11+)
├── kenaito-conf-cpp-client-demo/ # C++ 客户端示例
├── kenaito-conf-rust-client/ # Rust 客户端 SDK(Rust 2021+)
└── kenaito-conf-rust-client-demo/ # Rust 客户端示例
```
## 快速开始
+71 -9
View File
@@ -4,23 +4,28 @@
所有命令均在项目根目录执行。
## Go 客户端(测试通过)
## Go 客户端
```bash
docker build -f Dockerfile.go -t kenaito-conf-go-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-go-demo
kenaito-conf-golang-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-golang-demo
```
## Node.js 客户端(测试通过)
## Node.js 客户端
```bash
docker build -f Dockerfile.nodejs -t kenaito-conf-nodejs-demo .
# linux or mac
docker run -d --rm --name nodejs-demo \
-p 8083:8083 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
@@ -28,6 +33,9 @@ docker run -d --rm --name nodejs-demo \
-e KENAITO_ENV=daily \
kenaito-conf-nodejs-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-nodejs-demo
# 查看日志
docker logs -f nodejs-demo
@@ -39,11 +47,12 @@ curl http://localhost:8083/health
docker stop nodejs-demo
```
## Python 客户端(测试通过)
## Python 客户端
```bash
docker build -f Dockerfile.python -t kenaito-conf-python-demo .
# linux or mac
docker run -d --rm --name python-demo \
-p 8082:8082 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
@@ -51,6 +60,9 @@ docker run -d --rm --name python-demo \
-e KENAITO_ENV=daily \
kenaito-conf-python-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-python-demo
# 查看日志
docker logs -f python-demo
@@ -62,61 +74,76 @@ curl http://localhost:8082/health
docker stop python-demo
```
## .NET 客户端(测试通过)
## .NET 客户端
```bash
docker build -f Dockerfile.dotnet -t kenaito-conf-dotnet-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-dotnet-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-dotnet-demo
```
## Perl 客户端(测试通过)
## Perl 客户端
```bash
docker build -f Dockerfile.perl -t kenaito-conf-perl-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-perl-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-perl-demo
```
## Lua 客户端(测试通过)
## Lua 客户端
```bash
docker build -f Dockerfile.lua -t kenaito-conf-lua-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-lua-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-lua-demo
```
## PHP 客户端(测试通过)
## PHP 客户端
```bash
docker build -f Dockerfile.php -t kenaito-conf-php-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-php-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-php-demo
```
## Spring Boot 2 客户端(测试通过)
## Spring Boot 2 客户端
```bash
docker build -f Dockerfile.springboot2 -t kenaito-conf-springboot2-demo .
# linux or mac
docker run -d --rm --name springboot2-demo \
-p 8081:8081 \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
@@ -124,6 +151,9 @@ docker run -d --rm --name springboot2-demo \
-e KENAITO_ENV=daily \
kenaito-conf-springboot2-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-springboot2-demo
# 查看日志
docker logs -f springboot2-demo
@@ -135,6 +165,38 @@ curl http://localhost:8081/health
docker stop springboot2-demo
```
## C++ 客户端
```bash
docker build -f Dockerfile.cpp -t kenaito-conf-cpp-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-cpp-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-cpp-demo
```
## Rust 客户端(待测试)
```bash
docker build -f Dockerfile.rust -t kenaito-conf-rust-demo .
# linux or mac
docker run -it --rm \
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
-e KENAITO_APP_NAME=kenaito-springboot-demo \
-e KENAITO_ENV=daily \
kenaito-conf-rust-demo
# windows
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-rust-demo
```
## 环境变量说明
| 变量名 | 说明 | 默认值 |
+5 -1
View File
@@ -29,7 +29,11 @@ kenaito-conf/
├── kenaito-conf-dotnet-client/ # .NET client SDK (.NET 6.0+)
├── kenaito-conf-dotnet-client-demo/ # .NET client demo
├── kenaito-conf-lua-client/ # Lua client SDK (Lua 5.1+)
└── kenaito-conf-lua-client-demo/ # Lua client demo
├── kenaito-conf-lua-client-demo/ # Lua client demo
├── kenaito-conf-cpp-client/ # C++ client SDK (C++11+)
├── kenaito-conf-cpp-client-demo/ # C++ client demo
├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+)
└── kenaito-conf-rust-client-demo/ # Rust client demo
```
## Quick Start
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.10)
project(kenaito-conf-cpp-client-demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 引入客户端库
add_subdirectory(../kenaito-conf-cpp-client ${CMAKE_BINARY_DIR}/kenaito-conf-cpp-client)
add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE kenaito_conf_client)
+90
View File
@@ -0,0 +1,90 @@
#include <kenaito_conf/client.h>
#include <iostream>
#include <cstdlib>
#include <csignal>
#include <thread>
#include <chrono>
#include <map>
static kenaito::Client* g_client = nullptr;
static void signal_handler(int) {
std::cout << "\nShutting down..." << std::endl;
if (g_client) g_client->stop();
}
static std::string get_env(const char* key, const std::string& default_val) {
const char* val = std::getenv(key);
return (val && val[0] != '\0') ? val : default_val;
}
static int get_env_int(const char* key, int default_val) {
const char* val = std::getenv(key);
if (val && val[0] != '\0') {
try { return std::stoi(val); } catch (...) {}
}
return default_val;
}
int main() {
std::map<std::string, std::string> config_values = {
{"database.host", "192.168.1.1"},
{"database.port", "8888"},
{"database.name", "xxxx"},
{"database.user", "xxxx"},
{"database.password", "xxxxxxxx"},
};
kenaito::Config config;
config.base_url = get_env("KENAITO_BASE_URL", "http://localhost:8080");
config.app_name = get_env("KENAITO_APP_NAME", "kenaito-springboot-demo");
config.env = get_env("KENAITO_ENV", "daily");
config.version = get_env("KENAITO_VERSION", "latest");
config.token = get_env("KENAITO_TOKEN", "");
config.sync_interval = get_env_int("KENAITO_SYNC_INTERVAL", 10);
config.cache_dir = get_env("KENAITO_CACHE_DIR", ".");
kenaito::Client client(config);
g_client = &client;
client.on_change([&config_values](const std::map<std::string, std::string>& changed) {
std::cout << "Config changed:" << std::endl;
for (const auto& kv : changed) {
std::cout << " " << kv.first << " = " << kv.second << std::endl;
auto it = config_values.find(kv.first);
if (it != config_values.end()) {
it->second = kv.second;
std::cout << " [updated] " << kv.first << " = " << kv.second << std::endl;
}
}
});
if (!client.start()) {
std::cerr << "Failed to init config client, using local defaults" << std::endl;
}
auto remote = client.get_all();
for (auto& kv : config_values) {
auto it = remote.find(kv.first);
if (it != remote.end()) {
kv.second = it->second;
}
}
std::cout << "loaded " << remote.size() << " configs from remote" << std::endl;
std::cout << "\nCurrent configs:" << std::endl;
for (const auto& kv : config_values) {
std::cout << " " << kv.first << " = " << kv.second << std::endl;
}
std::cout << "\nListening for config changes... (press Ctrl+C to stop)" << std::endl;
std::signal(SIGINT, signal_handler);
while (true) {
client.sync_once();
std::this_thread::sleep_for(std::chrono::seconds(config.sync_interval));
}
return 0;
}
+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
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "kenaito-conf-rust-client-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
ctrlc = "3.4"
+85
View File
@@ -0,0 +1,85 @@
use kenaito_conf_client::{Client, Config};
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
fn get_env(key: &str, default: &str) -> String {
env::var(key).unwrap_or_else(|_| default.to_string())
}
fn get_env_int(key: &str, default: u64) -> u64 {
env::var(key)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
fn main() {
let config_values: HashMap<String, String> = [
("database.host".to_string(), "192.168.1.1".to_string()),
("database.port".to_string(), "8888".to_string()),
("database.name".to_string(), "xxxx".to_string()),
("database.user".to_string(), "xxxx".to_string()),
("database.password".to_string(), "xxxxxxxx".to_string()),
]
.iter()
.cloned()
.collect();
let config = Config {
base_url: get_env("KENAITO_BASE_URL", "http://localhost:8080"),
app_name: get_env("KENAITO_APP_NAME", "kenaito-springboot-demo"),
env: get_env("KENAITO_ENV", "daily"),
version: get_env("KENAITO_VERSION", "latest"),
token: get_env("KENAITO_TOKEN", ""),
sync_interval: get_env_int("KENAITO_SYNC_INTERVAL", 10),
cache_dir: get_env("KENAITO_CACHE_DIR", "."),
..Default::default()
};
let client = Arc::new(Client::new(config.clone()));
client.on_change(|changed| {
println!("Config changed:");
for (k, v) in changed {
println!(" {} = {}", k, v);
}
});
if let Err(e) = client.start() {
eprintln!("Failed to init config client, using local defaults: {}", e);
}
let mut config_values = config_values;
let remote = client.get_all();
for (k, v) in &remote {
if config_values.contains_key(k) {
config_values.insert(k.clone(), v.clone());
}
}
println!("loaded {} configs from remote", remote.len());
println!("\nCurrent configs:");
let mut keys: Vec<&String> = config_values.keys().collect();
keys.sort();
for k in keys {
println!(" {} = {}", k, config_values[k]);
}
println!("\nListening for config changes... (press Ctrl+C to stop)");
let client_clone = client.clone();
ctrlc::set_handler(move || {
println!("\nShutting down...");
client_clone.stop();
std::process::exit(0);
})
.expect("Error setting Ctrl-C handler");
loop {
client.sync_once();
thread::sleep(Duration::from_secs(config.sync_interval));
}
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "kenaito-conf-client"
version = "1.0.0"
edition = "2021"
description = "Kenaito Config Center Rust client SDK"
[dependencies]
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
+106
View File
@@ -0,0 +1,106 @@
# Kenaito Config Center Rust Client
Rust 客户端 SDK,适用于 Rust 2021 edition 项目。
## 功能特性
- 从配置中心拉取应用配置
- 定时同步配置变更
- 支持配置变更回调监听
- 本地文件缓存(配置中心不可用时降级)
## 依赖
- Rust 2021 edition
- reqwest (HTTP 客户端)
- serde / serde_json (JSON 处理)
## 使用
在你的 `Cargo.toml` 中添加:
```toml
[dependencies]
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
```
```rust
use kenaito_conf_client::{Config, Client};
fn main() {
let config = Config {
base_url: "http://localhost:8080".to_string(),
app_name: "my-app".to_string(),
env: "daily".to_string(),
sync_interval: 10,
cache_dir: ".".to_string(),
..Default::default()
};
let client = Client::new(config);
// 监听配置变更
client.on_change(|changed| {
println!("Config changed:");
for (k, v) in changed {
println!(" {} = {}", k, v);
}
});
// 启动客户端
if let Err(e) = client.start() {
eprintln!("Failed to start: {}", e);
return;
}
// 获取配置
let db_host = client.get("database.host");
println!("database.host = {}", db_host);
// 获取所有配置
let all = client.get_all();
for (k, v) in &all {
println!("{} = {}", k, v);
}
// 手动同步
client.sync_once();
// 停止
client.stop();
}
```
## 配置项
| 参数 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `base_url` | String | 配置中心地址 | `http://localhost:8080` |
| `app_name` | String | 应用名称 | (必填) |
| `env` | String | 环境标识 | `dev` |
| `version` | String | 配置版本 | `latest` |
| `token` | String | 应用 Token | 空 |
| `sync_interval` | u64 | 同步间隔(秒) | `60` |
| `ignore_global` | bool | 忽略全局配置 | `false` |
| `cache_dir` | String | 缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-rust-client-demo
cargo run
```
## 环境变量
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` | 缓存目录 | `.` |
+106
View File
@@ -0,0 +1,106 @@
# Kenaito Config Center Rust Client
Rust client SDK for the Kenaito Config Center, suitable for Rust 2021 edition projects.
## 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
- Rust 2021 edition
- reqwest (HTTP client)
- serde / serde_json (JSON processing)
## Usage
Add to your `Cargo.toml`:
```toml
[dependencies]
kenaito-conf-client = { path = "../kenaito-conf-rust-client" }
```
```rust
use kenaito_conf_client::{Config, Client};
fn main() {
let config = Config {
base_url: "http://localhost:8080".to_string(),
app_name: "my-app".to_string(),
env: "daily".to_string(),
sync_interval: 10,
cache_dir: ".".to_string(),
..Default::default()
};
let client = Client::new(config);
// Listen for config changes
client.on_change(|changed| {
println!("Config changed:");
for (k, v) in changed {
println!(" {} = {}", k, v);
}
});
// Start client
if let Err(e) = client.start() {
eprintln!("Failed to start: {}", e);
return;
}
// Get config
let db_host = client.get("database.host");
println!("database.host = {}", db_host);
// Get all configs
let all = client.get_all();
for (k, v) in &all {
println!("{} = {}", k, v);
}
// Manual sync
client.sync_once();
// Stop
client.stop();
}
```
## 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` | u64 | Sync interval (seconds) | `60` |
| `ignore_global` | bool | Whether to ignore global configs | `false` |
| `cache_dir` | String | Cache directory | `.` |
## Demo
```bash
cd kenaito-conf-rust-client-demo
cargo run
```
## 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 | `.` |
+312
View File
@@ -0,0 +1,312 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
const CACHE_FILE: &str = "config-cache.json";
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: String,
pub app_name: String,
pub env: String,
pub version: String,
pub token: String,
pub sync_interval: u64,
pub ignore_global: bool,
pub cache_dir: String,
}
impl Default for Config {
fn default() -> Self {
Self {
base_url: "http://localhost:8080".to_string(),
app_name: String::new(),
env: "dev".to_string(),
version: "latest".to_string(),
token: String::new(),
sync_interval: 60,
ignore_global: false,
cache_dir: ".".to_string(),
}
}
}
type ChangeHandler = Box<dyn Fn(&HashMap<String, String>) + Send + Sync + 'static>;
struct Cache {
dir: String,
}
impl Cache {
fn new(dir: &str) -> Self {
Self { dir: dir.to_string() }
}
fn write(&self, data: &str) -> Result<(), std::io::Error> {
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
let mut file = fs::File::create(path)?;
file.write_all(data.as_bytes())?;
Ok(())
}
fn read(&self) -> Option<String> {
let path = PathBuf::from(&self.dir).join(CACHE_FILE);
let mut file = fs::File::open(path).ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;
Some(contents)
}
}
pub struct Client {
config: Config,
cache: Cache,
handlers: Arc<Mutex<Vec<ChangeHandler>>>,
configs: Arc<Mutex<HashMap<String, String>>>,
last_time: Arc<Mutex<i64>>,
stop_flag: Arc<Mutex<bool>>,
}
impl Client {
pub fn new(config: Config) -> Self {
let cache_dir = if config.cache_dir.is_empty() { ".".to_string() } else { config.cache_dir.clone() };
let version = if config.version.is_empty() { "latest".to_string() } else { config.version.clone() };
let sync_interval = if config.sync_interval == 0 { 60 } else { config.sync_interval };
let mut cfg = config;
cfg.cache_dir = cache_dir;
cfg.version = version;
cfg.sync_interval = sync_interval;
Self {
cache: Cache::new(&cfg.cache_dir),
config: cfg,
handlers: Arc::new(Mutex::new(Vec::new())),
configs: Arc::new(Mutex::new(HashMap::new())),
last_time: Arc::new(Mutex::new(0)),
stop_flag: Arc::new(Mutex::new(false)),
}
}
pub fn on_change<F>(&self, handler: F)
where
F: Fn(&HashMap<String, String>) + Send + Sync + 'static,
{
let mut handlers = self.handlers.lock().unwrap();
handlers.push(Box::new(handler));
}
pub fn get(&self, key: &str) -> String {
let configs = self.configs.lock().unwrap();
configs.get(key).cloned().unwrap_or_default()
}
pub fn get_all(&self) -> HashMap<String, String> {
self.configs.lock().unwrap().clone()
}
pub fn start(&self) -> Result<(), String> {
match self.fetch_all() {
Ok(_) => {}
Err(e) => {
eprintln!("[ConfigClient] fetch all configs error: {}, trying cache", e);
match self.cache.read() {
Some(cached) => {
match serde_json::from_str::<serde_json::Value>(&cached) {
Ok(val) => {
let mut data = HashMap::new();
if let Some(obj) = val.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
data.insert(k.clone(), s.to_string());
}
}
}
self.apply_configs(&data);
eprintln!("[ConfigClient] loaded {} configs from cache", self.configs.lock().unwrap().len());
}
Err(_) => return Err("fetch configs failed and no cache".to_string()),
}
}
None => return Err("fetch configs failed and no cache".to_string()),
}
}
}
Ok(())
}
pub fn sync_loop(&self) {
let interval = Duration::from_secs(self.config.sync_interval);
loop {
thread::sleep(interval);
if *self.stop_flag.lock().unwrap() {
break;
}
if let Err(e) = self.fetch_changed() {
eprintln!("[ConfigClient] sync error: {}", e);
}
}
}
pub fn sync_once(&self) {
if let Err(e) = self.fetch_changed() {
eprintln!("[ConfigClient] sync error: {}", e);
}
}
pub fn stop(&self) {
*self.stop_flag.lock().unwrap() = true;
}
fn http_post(&self, url: &str, body: &str) -> Result<String, String> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| e.to_string())?;
let resp = client
.post(url)
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("HTTP {}", resp.status()));
}
resp.text().map_err(|e| e.to_string())
}
fn fetch_all(&self) -> Result<(), String> {
let url = format!("{}/api/fetchAllConfigs", self.config.base_url);
let payload = serde_json::json!({
"appName": self.config.app_name,
"env": self.config.env,
"version": self.config.version,
"ignoreGlobal": self.config.ignore_global,
"token": self.config.token,
});
let resp = self.http_post(&url, &payload.to_string())?;
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
let msg = val.get("msg").and_then(|m| m.as_str()).unwrap_or("unknown");
return Err(format!("server error: {}", msg));
}
}
if let Some(ts) = val.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
if let Ok(t) = ts.parse::<i64>() {
*self.last_time.lock().unwrap() = t;
}
}
// cache raw response
let _ = self.cache.write(&resp);
// apply configs
let mut data = HashMap::new();
if let Some(obj) = val.as_object() {
for (k, v) in obj {
if let Some(s) = v.as_str() {
data.insert(k.clone(), s.to_string());
}
}
}
self.apply_configs(&data);
eprintln!("[ConfigClient] fetched {} configs", self.configs.lock().unwrap().len());
Ok(())
}
fn fetch_changed(&self) -> Result<(), String> {
let url = format!("{}/api/fetchChangedConfigs", self.config.base_url);
let payload = serde_json::json!({
"appName": self.config.app_name,
"env": self.config.env,
"version": self.config.version,
"lastTime": *self.last_time.lock().unwrap(),
"ignoreGlobal": self.config.ignore_global,
});
let resp = self.http_post(&url, &payload.to_string())?;
let val: serde_json::Value = serde_json::from_str(&resp).map_err(|e| e.to_string())?;
if let Some(code) = val.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
return Err(format!("server error code: {}", code));
}
}
let data = val.get("data").and_then(|d| d.as_object()).cloned().unwrap_or_default();
if let Some(ts) = data.get("latest.fetch.timestamp").and_then(|t| t.as_str()) {
if let Ok(t) = ts.parse::<i64>() {
*self.last_time.lock().unwrap() = t;
}
}
// handle deleted keys
if let Some(deleted_str) = data.get("deletedKeys").and_then(|d| d.as_str()) {
if !deleted_str.is_empty() {
let deleted_keys: Vec<&str> = deleted_str.split(',').collect();
let mut configs = self.configs.lock().unwrap();
for key in &deleted_keys {
configs.remove(*key);
}
eprintln!("[ConfigClient] {} configs deleted", deleted_keys.len());
}
}
// handle changed configs
let mut changed = HashMap::new();
for (k, v) in &data {
if k == "latest.fetch.timestamp" || k == "deletedKeys" {
continue;
}
if let Some(s) = v.as_str() {
changed.insert(k.clone(), s.to_string());
}
}
if !changed.is_empty() {
{
let mut configs = self.configs.lock().unwrap();
for (k, v) in &changed {
configs.insert(k.clone(), v.clone());
}
}
let handlers = self.handlers.lock().unwrap();
for h in handlers.iter() {
h(&changed);
}
let keys: Vec<&str> = changed.keys().map(|s| s.as_str()).collect();
eprintln!("[ConfigClient] {} configs changed: {}", changed.len(), keys.join(", "));
}
Ok(())
}
fn apply_configs(&self, data: &HashMap<String, String>) {
let mut configs = self.configs.lock().unwrap();
for (k, v) in data {
if k.starts_with("kenaito.") || k == "latest.fetch.timestamp" {
continue;
}
configs.insert(k.clone(), v.clone());
}
}
}