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:
@@ -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
|
||||
Reference in New Issue
Block a user