chore: 删除多个客户端模块

移除以下未完成的客户端模块:
- C++ 客户端和 demo
- .NET 客户端和 demo
- Kotlin 客户端和 demo
- Lua 客户端和 demo
- PHP 客户端和 demo
- Ruby 客户端和 demo
- Rust 客户端和 demo

更新根目录 README.md,移除相关说明。
This commit is contained in:
2026-04-25 21:05:00 +08:00
parent 7efca9440a
commit b4b38fa0c7
36 changed files with 27 additions and 2278 deletions
+27 -263
View File
@@ -14,30 +14,30 @@
kenaito-conf/
├── server/ # Go 管理端(Gin + xorm)
├── web/ # Vue 2 前端
├── kenaito-conf-go-client/ # (测试通过)Go 客户端 SDK
├── kenaito-conf-go-client-demo/ # (测试通过)Go 客户端示例
├── kenaito-conf-springboot2-client/ # (测试通过)Java Spring Boot 2.x 客户端 SDK(JDK 8+)
├── kenaito-conf-springboot2-client-demo/# (测试通过)Spring Boot 2.x 客户端示例(端口 8081)
├── kenaito-conf-python-client/ # (测试通过)Python 客户端 SDK(Python 3.10+)
├── kenaito-conf-python-client-demo/ # (测试通过)Python 客户端示例(端口 8082)
├── kenaito-conf-nodejs-client/ # (测试通过)Node.js 客户端 SDK(Node.js 18+)
├── kenaito-conf-nodejs-client-demo/ # (测试通过)Node.js 客户端示例(端口 8083)
├── kenaito-conf-php-client/ # (待测试)PHP 客户端 SDK(PHP 8.0+)
├── kenaito-conf-php-client-demo/ # (待测试)PHP 客户端示例
├── kenaito-conf-dotnet-client/ # (待测试).NET 客户端 SDK(.NET 6.0+)
├── kenaito-conf-dotnet-client-demo/ # (待测试).NET 客户端示例
├── kenaito-conf-c-client/ # (待测试)C 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-c-client-demo/ # (待测试)C 客户端示例
├── kenaito-conf-cpp-client/ # (待测试)C++ 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-cpp-client-demo/ # (待测试)C++ 客户端示例
├── kenaito-conf-lua-client/ # (待测试)Lua 客户端 SDK(Lua 5.3+)
├── kenaito-conf-lua-client-demo/ # (待测试)Lua 客户端示例
├── kenaito-conf-kotlin-client/ # (待测试)Kotlin 客户端 SDK(Kotlin 1.9+)
├── kenaito-conf-kotlin-client-demo/ # (待测试)Kotlin 客户端示例
├── kenaito-conf-rust-client/ # (待测试)Rust 客户端 SDK(Rust 1.70+)
├── kenaito-conf-rust-client-demo/ # (待测试)Rust 客户端示例
├── kenaito-conf-ruby-client/ # (待测试)Ruby 客户端 SDK(Ruby 2.7+)
└── kenaito-conf-ruby-client-demo/ # (待测试)Ruby 客户端示例
├── kenaito-conf-go-client/ # Go 客户端 SDK
├── kenaito-conf-go-client-demo/ # Go 客户端示例
├── kenaito-conf-springboot2-client/ # Java Spring Boot 2.x 客户端 SDK(JDK 8+)
├── kenaito-conf-springboot2-client-demo/# Spring Boot 2.x 客户端示例(端口 8081)
├── kenaito-conf-python-client/ # Python 客户端 SDK(Python 3.10+)
├── kenaito-conf-python-client-demo/ # Python 客户端示例(端口 8082)
├── kenaito-conf-nodejs-client/ # Node.js 客户端 SDK(Node.js 18+)
├── kenaito-conf-nodejs-client-demo/ # Node.js 客户端示例(端口 8083)
├── kenaito-conf-php-client/ # (未支持)PHP 客户端 SDK(PHP 8.0+)
├── kenaito-conf-php-client-demo/ # (未支持)PHP 客户端示例
├── kenaito-conf-dotnet-client/ # (未支持).NET 客户端 SDK(.NET 6.0+)
├── kenaito-conf-dotnet-client-demo/ # (未支持).NET 客户端示例
├── kenaito-conf-c-client/ # (未支持)C 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-c-client-demo/ # (未支持)C 客户端示例
├── kenaito-conf-cpp-client/ # (未支持)C++ 客户端 SDK(libcurl + OpenSSL)
├── kenaito-conf-cpp-client-demo/ # (未支持)C++ 客户端示例
├── kenaito-conf-lua-client/ # (未支持)Lua 客户端 SDK(Lua 5.3+)
├── kenaito-conf-lua-client-demo/ # (未支持)Lua 客户端示例
├── kenaito-conf-kotlin-client/ # (未支持)Kotlin 客户端 SDK(Kotlin 1.9+)
├── kenaito-conf-kotlin-client-demo/ # (未支持)Kotlin 客户端示例
├── kenaito-conf-rust-client/ # (未支持)Rust 客户端 SDK(Rust 1.70+)
├── kenaito-conf-rust-client-demo/ # (未支持)Rust 客户端示例
├── kenaito-conf-ruby-client/ # (未支持)Ruby 客户端 SDK(Ruby 2.7+)
└── kenaito-conf-ruby-client-demo/ # (未支持)Ruby 客户端示例
```
## 快速开始
@@ -132,6 +132,7 @@ dbHost := c.Get("db.host")
**1. 引入依赖**
```xml
<dependency>
<groupId>cn.odboy</groupId>
<artifactId>kenaito-conf-springboot2-client</artifactId>
@@ -166,26 +167,8 @@ private String databaseHost;
private Integer databasePort;
```
配置变更时会自动刷新 `@Value` 字段,无需重启应用。详见 [Spring Boot 2 客户端文档](kenaito-conf-springboot2-client/README.md)。
### Java Spring Boot 3 客户端
适用于 JDK 11+ 项目(当前编译目标 Spring Boot 2.7.18,预留 Spring Boot 3 升级路径)。
**1. 引入依赖**
```xml
<dependency>
<groupId>cn.odboy</groupId>
<artifactId>kenaito-conf-springboot3-client</artifactId>
<version>1.0.0</version>
</dependency>
```
**2. 配置与使用方式同 Spring Boot 2.x 客户端。**
> **注意:** 升级到 Spring Boot 3 需先将编译目标改为 JDK 17,并更新 `spring-boot.version` 为 3.x、Spring Framework 为 6.x。
配置变更时会自动刷新 `@Value`
字段,无需重启应用。详见 [Spring Boot 2 客户端文档](kenaito-conf-springboot2-client/README.md)。
### Python 客户端
@@ -281,225 +264,6 @@ const allConfigs = client.getAll();
client.stop();
```
### PHP 客户端
适用于 PHP 8.0+ 项目。
**1. 安装**
```bash
cd kenaito-conf-php-client
composer install
```
**2. 使用**
```php
use KenaitoConf\Config;
use KenaitoConf\ConfigClient;
$config = new Config([
'baseUrl' => 'http://localhost:8080',
'appName' => 'kenaito-springboot-demo',
'env' => 'daily',
'syncInterval' => 30,
]);
$client = new ConfigClient($config);
$client->onChange(function ($changed) {
foreach ($changed as $k => $v) {
echo "config changed: $k = $v\n";
}
});
$client->start();
$value = $client->get('database.host');
$all = $client->getAll();
```
### .NET 客户端
适用于 .NET 6.0+ 项目。
**1. 引入依赖**
```xml
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj"/>
```
**2. 使用**
```csharp
using KenaitoConf;
var config = new Config
{
BaseUrl = "http://localhost:8080",
AppName = "kenaito-springboot-demo",
Env = "daily",
SyncInterval = 30,
};
var client = new ConfigClient(config);
client.OnChange(changed => {
foreach (var kv in changed)
Console.WriteLine($"config changed: {kv.Key} = {kv.Value}");
});
await client.StartAsync();
var value = client.Get("database.host");
```
### C 客户端
适用于嵌入式或高性能场景,依赖 libcurl。
**编译**
```bash
gcc -o demo kenaito-conf-c-client/main.c -lcurl
```
### C++ 客户端
依赖 libcurl,C++17。
**编译**
```bash
g++ -std=c++17 -o demo kenaito-conf-cpp-client/main.cpp -lcurl -lpthread
```
### Lua 客户端
适用于 Lua 5.3+ 项目(如 OpenResty/Nginx 网关)。
**1. 安装**
```bash
cd kenaito-conf-lua-client
luarocks install kenaito-conf-client-1.0.0-1.rockspec
```
**2. 使用**
```lua
local ConfigClient = require("kenaito-conf")
local config = {
baseUrl = "http://localhost:8080",
appName = "kenaito-springboot-demo",
env = "daily",
syncInterval = 30,
}
local client = ConfigClient.new(config)
client:on_change(function(changed)
for k, v in pairs(changed) do
print("config changed: " .. k .. " = " .. v)
end
end)
client:start()
```
### Kotlin 客户端
适用于 Kotlin 1.9+ 项目。
**1. 编译**
```bash
cd kenaito-conf-kotlin-client
./gradlew build
```
**2. 使用**
```kotlin
import kenait.conf.Config
import kenait.conf.ConfigClient
val config = Config(
baseUrl = "http://localhost:8080",
appName = "kenaito-springboot-demo",
env = "daily",
syncInterval = 30,
)
val client = ConfigClient(config)
client.onChange { changed ->
changed.forEach { (k, v) -> println("config changed: $k = $v") }
}
client.start()
val value = client.get("database.host")
```
### Rust 客户端
适用于 Rust 1.70+ 项目。
**1. 在 Cargo.toml 中添加**
```toml
[dependencies]
kenaito-conf = { path = "../kenaito-conf-rust-client" }
```
**2. 使用**
```rust
use kenaito_conf::{Config, ConfigClient};
let config = Config {
base_url: "http://localhost:8080".to_string(),
app_name: "kenaito-springboot-demo".to_string(),
env: "daily".to_string(),
sync_interval: 30,
..Default::default()
};
let client = ConfigClient::new(config);
client.on_change(|changed| {
for (k, v) in changed { println!("config changed: {} = {}", k, v); }
});
client.start().await.unwrap();
let value = client.get("database.host");
```
### Ruby 客户端
适用于 Ruby 2.7+ 项目。
**1. 安装**
```bash
cd kenaito-conf-ruby-client
gem build kenaito-conf.gemspec
gem install kenaito-conf-1.0.0.gem
```
**2. 使用**
```ruby
require 'kenaito_conf'
config = KenaitoConf::Config.new(
base_url: 'http://localhost:8080',
app_name: 'kenaito-springboot-demo',
env: 'daily',
sync_interval: 30
)
client = KenaitoConf::ConfigClient.new(config)
client.on_change do |changed|
changed.each { |k, v| puts "config changed: #{k} = #{v}" }
end
client.start
value = client.get('database.host')
```
## API 概览
### 管理端 API(需 JWT 认证)
-8
View File
@@ -1,8 +0,0 @@
#include <iostream>
int main() {
std::cout << "Kenaito Config Center C++ Client Demo" << std::endl;
std::cout << "AppName: kenaito-springboot-demo, Env: daily" << std::endl;
std::cout << "See ../kenaito-conf-cpp-client/main.cpp for full implementation" << std::endl;
return 0;
}
-48
View File
@@ -1,48 +0,0 @@
# Kenaito Config Center C++ Client
C++ 语言的配置中心客户端 SDK,使用 libcurl。
## 依赖
- libcurl
- C++17
### macOS
```bash
brew install curl
```
### Ubuntu/Debian
```bash
apt-get install libcurl4-openssl-dev
```
## 编译
```bash
cd kenaito-conf-cpp-client-demo
g++ -std=c++17 -o demo ../kenaito-conf-cpp-client/main.cpp -lcurl -lpthread
./demo
```
## 功能
- HTTP POST 拉取配置
- 定时同步
- 变更回调
- 线程安全
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `baseUrl` | 配置中心地址 | 必填 |
| `appName` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `syncInterval` | 同步间隔(秒) | `60` |
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
| `cacheDir` | 本地缓存目录 | `.` |
-106
View File
@@ -1,106 +0,0 @@
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <functional>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <fstream>
#include <sstream>
#include <cstring>
#include <curl/curl.h>
namespace kenaito_conf {
struct Config {
std::string baseUrl;
std::string appName;
std::string env = "dev";
std::string version = "latest";
std::string token;
int syncInterval = 60;
bool ignoreGlobal = false;
std::string cacheDir = ".";
};
class ConfigClient {
public:
ConfigClient(const Config &config) : config_(config) {}
void onChange(std::function<void(const std::map<std::string, std::string> &)> handler) {
handlers_.push_back(handler);
}
std::string get(const std::string &key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = configs_.find(key);
return it != configs_.end() ? it->second : "";
}
std::map<std::string, std::string> getAll() {
std::lock_guard<std::mutex> lock(mutex_);
return configs_;
}
void start() {
if (started_) return;
started_ = true;
fetchAll();
running_ = true;
syncThread_ = std::thread([this]() {
while (running_) {
std::this_thread::sleep_for(std::chrono::seconds(config_.syncInterval));
if (!running_) break;
try { fetchChanged(); } catch (...) {}
}
});
}
void stop() {
running_ = false;
if (syncThread_.joinable()) syncThread_.join();
}
private:
Config config_;
std::map<std::string, std::string> configs_;
long lastTime_ = 0;
bool started_ = false;
std::atomic<bool> running_{false};
std::mutex mutex_;
std::thread syncThread_;
std::vector<std::function<void(const std::map<std::string, std::string> &)>> handlers_;
void fetchAll() { /* HTTP POST to /api/fetchAllConfigs */ }
void fetchChanged() { /* HTTP POST to /api/fetchChangedConfigs */ }
};
} // namespace kenaito_conf
int main() {
std::cout << "Kenaito Config Center C++ Client Demo" << std::endl;
kenaito_conf::Config config;
config.baseUrl = "http://localhost:8080";
config.appName = "kenaito-springboot-demo";
config.env = "daily";
config.version = "latest";
config.syncInterval = 30;
kenaito_conf::ConfigClient client(config);
client.onChange([](const std::map<std::string, std::string> &changed) {
for (auto &[k, v] : changed)
std::cout << "config changed: " << k << " = " << v << std::endl;
});
client.start();
std::cout << "database.host = " << client.get("database.host") << std::endl;
std::cout << "Listening for changes... (press Ctrl+C to stop)" << std::endl;
std::cin.get();
client.stop();
return 0;
}
@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj" />
</ItemGroup>
</Project>
@@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using KenaitoConf;
var configValues = new Dictionary<string, string>
{
{ "database.host", Environment.GetEnvironmentVariable("DATABASE_HOST") ?? "192.168.1.1" },
{ "database.port", Environment.GetEnvironmentVariable("DATABASE_PORT") ?? "8888" },
{ "database.name", Environment.GetEnvironmentVariable("DATABASE_NAME") ?? "xxxx" },
{ "database.user", Environment.GetEnvironmentVariable("DATABASE_USER") ?? "xxxx" },
{ "database.password", Environment.GetEnvironmentVariable("DATABASE_PASSWORD") ?? "xxxxxxxx" },
};
var config = new Config
{
BaseUrl = Environment.GetEnvironmentVariable("KENAITO_BASE_URL") ?? "http://localhost:8080",
AppName = Environment.GetEnvironmentVariable("KENAITO_APP_NAME") ?? "kenaito-springboot-demo",
Env = Environment.GetEnvironmentVariable("KENAITO_PROFILE") ?? "daily",
Version = Environment.GetEnvironmentVariable("KENAITO_VERSION") ?? "latest",
Token = Environment.GetEnvironmentVariable("KENAITO_TOKEN") ?? "",
SyncInterval = int.Parse(Environment.GetEnvironmentVariable("KENAITO_SYNC_INTERVAL") ?? "10"),
CacheDir = Environment.GetEnvironmentVariable("KENAITO_CACHE_DIR") ?? ".",
};
var client = new ConfigClient(config);
client.OnChange(changed =>
{
foreach (var kv in changed)
{
if (configValues.ContainsKey(kv.Key))
{
configValues[kv.Key] = kv.Value;
Console.Error.WriteLine($"config updated: {kv.Key} = {kv.Value}");
}
}
});
try
{
await client.StartAsync();
var remote = client.GetAll();
foreach (var key in new List<string>(configValues.Keys))
{
if (remote.ContainsKey(key))
configValues[key] = remote[key];
}
Console.Error.WriteLine($"loaded {remote.Count} configs from remote");
}
catch (Exception e)
{
Console.Error.WriteLine($"failed to init config client: {e.Message}");
}
Console.WriteLine("Current configs:");
foreach (var kv in configValues)
Console.WriteLine($" {kv.Key} = {kv.Value}");
Console.WriteLine("\nListening for config changes... (press Ctrl+C to stop)");
var cts = new System.Threading.CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
try { await System.Threading.Tasks.Task.Delay(System.Threading.Timeout.Infinite, cts.Token); }
catch (System.Threading.Tasks.TaskCanceledException) { }
client.Stop();
Console.WriteLine("Shutting down...");
-219
View File
@@ -1,219 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace KenaitoConf
{
public class Config
{
public string BaseUrl { get; set; } = "";
public string AppName { get; set; } = "";
public string Env { get; set; } = "dev";
public string Version { get; set; } = "latest";
public string Token { get; set; } = "";
public int SyncInterval { get; set; } = 60;
public bool IgnoreGlobal { get; set; } = false;
public string CacheDir { get; set; } = ".";
}
public class Cache
{
private readonly string _cacheDir;
private const string CacheFile = "config-cache.json";
public Cache(string cacheDir = ".")
{
_cacheDir = cacheDir;
}
public void Write(Dictionary<string, object> data)
{
string path = Path.Combine(_cacheDir, CacheFile);
File.WriteAllText(path, JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
}
public Dictionary<string, object> Read()
{
string path = Path.Combine(_cacheDir, CacheFile);
if (!File.Exists(path)) return null;
string content = File.ReadAllText(path);
return JsonSerializer.Deserialize<Dictionary<string, object>>(content);
}
}
public class ConfigClient
{
private readonly Config _config;
private readonly Cache _cache;
private readonly HttpClient _http = new();
private readonly Dictionary<string, string> _configs = new();
private long _lastTime = 0;
private bool _started = false;
private CancellationTokenSource _cts;
private readonly List<Action<Dictionary<string, string>>> _handlers = new();
public ConfigClient(Config config)
{
_config = config;
_cache = new Cache(config.CacheDir);
_http.Timeout = TimeSpan.FromSeconds(10);
}
public void OnChange(Action<Dictionary<string, string>> handler)
{
_handlers.Add(handler);
}
public string Get(string key)
{
return _configs.TryGetValue(key, out var value) ? value : null;
}
public Dictionary<string, string> GetAll()
{
return new Dictionary<string, string>(_configs);
}
public async Task StartAsync()
{
if (_started) return;
_started = true;
try
{
await FetchAllAsync();
}
catch (Exception e)
{
Console.Error.WriteLine($"[ConfigClient] fetch all configs error: {e.Message}, trying cache");
var cached = _cache.Read();
if (cached != null)
{
ApplyConfigs(cached);
Console.Error.WriteLine($"[ConfigClient] loaded {_configs.Count} configs from cache");
}
else
{
throw new Exception($"fetch configs failed and no cache: {e.Message}");
}
}
_cts = new CancellationTokenSource();
_ = SyncLoopAsync(_cts.Token);
}
public void Stop()
{
_cts?.Cancel();
}
private async Task SyncLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(_config.SyncInterval * 1000, ct);
try
{
await FetchChangedAsync();
}
catch (Exception e)
{
Console.Error.WriteLine($"[ConfigClient] sync error: {e.Message}");
}
}
}
private async Task FetchAllAsync()
{
string url = $"{_config.BaseUrl}/api/fetchAllConfigs";
var payload = new
{
appName = _config.AppName,
env = _config.Env,
version = _config.Version,
ignoreGlobal = _config.IgnoreGlobal,
token = _config.Token
};
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var resp = await _http.PostAsync(url, content);
resp.EnsureSuccessStatusCode();
string body = await resp.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<Dictionary<string, object>>(body);
if (data.ContainsKey("code"))
{
var code = Convert.ToInt32(data["code"]);
if (code != 0)
throw new Exception($"server error: {(data.ContainsKey("msg") ? data["msg"] : "unknown")}");
}
if (data.TryGetValue("latest.fetch.timestamp", out var ts))
_lastTime = Convert.ToInt64(ts);
_cache.Write(data);
ApplyConfigs(data);
Console.Error.WriteLine($"[ConfigClient] fetched {_configs.Count} configs");
}
private async Task FetchChangedAsync()
{
string url = $"{_config.BaseUrl}/api/fetchChangedConfigs";
var payload = new
{
appName = _config.AppName,
env = _config.Env,
version = _config.Version,
lastTime = _lastTime,
ignoreGlobal = _config.IgnoreGlobal,
token = _config.Token
};
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var resp = await _http.PostAsync(url, content);
resp.EnsureSuccessStatusCode();
string body = await resp.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Dictionary<string, object>>(body);
if (!result.ContainsKey("code") || Convert.ToInt32(result["code"]) != 0)
throw new Exception("server error");
var changedData = JsonSerializer.Deserialize<Dictionary<string, object>>(result["data"].ToString());
if (changedData.TryGetValue("latest.fetch.timestamp", out var ts))
{
_lastTime = Convert.ToInt64(ts);
changedData.Remove("latest.fetch.timestamp");
}
var changed = new Dictionary<string, string>();
foreach (var kv in changedData)
{
changed[kv.Key] = kv.Value?.ToString() ?? "";
}
foreach (var kv in changed)
_configs[kv.Key] = kv.Value;
foreach (var handler in _handlers)
{
try { handler(changed); }
catch (Exception e) { Console.Error.WriteLine($"[ConfigClient] handler error: {e.Message}"); }
}
}
private void ApplyConfigs(Dictionary<string, object> data)
{
foreach (var kv in data)
{
if (kv.Key.StartsWith("kenaito.") || kv.Key == "latest.fetch.timestamp")
continue;
if (kv.Value is JsonElement)
continue;
_configs[kv.Key] = kv.Value?.ToString() ?? "";
}
}
}
}
@@ -1,7 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>KenaitoConf</RootNamespace>
<AssemblyName>KenaitoConf</AssemblyName>
</PropertyGroup>
</Project>
-76
View File
@@ -1,76 +0,0 @@
# Kenaito Config Center .NET Client
.NET 语言的配置中心客户端 SDK,支持 .NET 6.0+。
## 依赖
- .NET 6.0+
- System.Text.Json(内置)
## 安装
### 项目引用
在 `.csproj` 中添加:
```xml
<ItemGroup>
<ProjectReference Include="..\kenaito-conf-dotnet-client\KenaitoConf.csproj" />
</ItemGroup>
```
### NuGet 包(待发布)
```bash
dotnet add package KenaitoConf
```
## 使用
```csharp
using KenaitoConf;
var config = new Config
{
BaseUrl = "http://localhost:8080",
AppName = "kenaito-springboot-demo",
Env = "daily",
Version = "latest",
SyncInterval = 30,
};
var client = new ConfigClient(config);
client.OnChange(changed =>
{
foreach (var kv in changed)
Console.WriteLine($"config changed: {kv.Key} = {kv.Value}");
});
await client.StartAsync();
var value = client.Get("database.host");
var all = client.GetAll();
client.Stop();
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `BaseUrl` | 配置中心地址 | 必填 |
| `AppName` | 应用名称 | 必填 |
| `Env` | 环境 | `dev` |
| `Version` | 配置版本 | `latest` |
| `Token` | 认证令牌 | 空 |
| `SyncInterval` | 同步间隔(秒) | `60` |
| `IgnoreGlobal` | 是否忽略全局配置 | `false` |
| `CacheDir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-dotnet-client-demo
dotnet run
```
@@ -1,14 +0,0 @@
plugins {
kotlin("jvm") version "1.9.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation(files("../kenaito-conf-kotlin-client/build/libs/kenaito-conf-kotlin-client-1.0.0.jar"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
application { mainClass.set("demo.AppKt") }
@@ -1 +0,0 @@
rootProject.name = "kenaito-conf-kotlin-client-demo"
@@ -1,49 +0,0 @@
import kenait.conf.Config
import kenait.conf.ConfigClient
fun main() {
val configValues = mutableMapOf(
"database.host" to (System.getenv("DATABASE_HOST") ?: "192.168.1.1"),
"database.port" to (System.getenv("DATABASE_PORT") ?: "8888"),
"database.name" to (System.getenv("DATABASE_NAME") ?: "xxxx"),
"database.user" to (System.getenv("DATABASE_USER") ?: "xxxx"),
"database.password" to (System.getenv("DATABASE_PASSWORD") ?: "xxxxxxxx"),
)
val config = Config(
baseUrl = System.getenv("KENAITO_BASE_URL") ?: "http://localhost:8080",
appName = System.getenv("KENAITO_APP_NAME") ?: "kenaito-springboot-demo",
env = System.getenv("KENAITO_PROFILE") ?: "daily",
version = System.getenv("KENAITO_VERSION") ?: "latest",
token = System.getenv("KENAITO_TOKEN") ?: "",
syncInterval = (System.getenv("KENAITO_SYNC_INTERVAL") ?: "10").toInt(),
cacheDir = System.getenv("KENAITO_CACHE_DIR") ?: ".",
)
val client = ConfigClient(config)
client.onChange { changed ->
changed.forEach { (k, v) ->
if (configValues.containsKey(k)) {
configValues[k] = v
System.err.println("config updated: $k = $v")
}
}
}
try {
client.start()
val remote = client.getAll()
configValues.keys.forEach { k ->
remote[k]?.let { configValues[k] = it }
}
System.err.println("loaded ${remote.size} configs from remote")
} catch (e: Exception) {
System.err.println("failed to init config client: ${e.message}")
}
println("Current configs:")
configValues.forEach { (k, v) -> println(" $k = $v") }
println("\nListening for config changes... (press Ctrl+C to stop)")
Thread.currentThread().join()
}
-66
View File
@@ -1,66 +0,0 @@
# Kenaito Config Center Kotlin Client
Kotlin 语言的配置中心客户端 SDK。
## 依赖
- Kotlin 1.9+
- kotlinx-coroutines
- kotlinx-serialization
## 编译
```bash
cd kenaito-conf-kotlin-client
./gradlew build
```
## 使用
```kotlin
import kenait.conf.Config
import kenait.conf.ConfigClient
val config = Config(
baseUrl = "http://localhost:8080",
appName = "kenaito-springboot-demo",
env = "daily",
version = "latest",
syncInterval = 30,
)
val client = ConfigClient(config)
client.onChange { changed ->
changed.forEach { (k, v) ->
println("config changed: $k = $v")
}
}
client.start()
val value = client.get("database.host")
val all = client.getAll()
client.stop()
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `baseUrl` | 配置中心地址 | 必填 |
| `appName` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `syncInterval` | 同步间隔(秒) | `60` |
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
| `cacheDir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-kotlin-client-demo
./gradlew run
```
@@ -1,14 +0,0 @@
plugins {
kotlin("jvm") version "1.9.0"
kotlin("plugin.serialization") version "1.9.0"
}
group = "cn.odboy"
version = "1.0.0"
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
@@ -1 +0,0 @@
rootProject.name = "kenaito-conf-kotlin-client"
@@ -1,107 +0,0 @@
package kenaito.conf
import kotlinx.coroutines.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.File
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
@Serializable
data class Config(
val baseUrl: String = "",
val appName: String = "",
val env: String = "dev",
val version: String = "latest",
val token: String = "",
val syncInterval: Int = 60,
val ignoreGlobal: Boolean = false,
val cacheDir: String = "."
)
class Cache(private val cacheDir: String) {
private val cacheFile = File(cacheDir, "config-cache.json")
fun write(data: String) { cacheFile.writeText(data) }
fun read(): String? = if (cacheFile.exists()) cacheFile.readText() else null
}
class ConfigClient(private val config: Config) {
private val httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
private val configs = ConcurrentHashMap<String, String>()
private var lastTime = 0L
private var started = false
private val handlers = mutableListOf<(Map<String, String>) -> Unit>()
private val json = Json { ignoreUnknownKeys = true }
fun onChange(handler: (Map<String, String>) -> Unit) { handlers.add(handler) }
fun get(key: String): String? = configs[key]
fun getAll(): Map<String, String> = HashMap(configs)
fun start() {
if (started) return
started = true
try { fetchAll() } catch (e: Exception) {
System.err.println("[ConfigClient] fetch all configs error: ${e.message}, trying cache")
val cached = Cache(config.cacheDir).read()
if (cached != null) {
applyConfigs(cached)
System.err.println("[ConfigClient] loaded ${configs.size} configs from cache")
} else throw RuntimeException("fetch configs failed and no cache: ${e.message}")
}
CoroutineScope(Dispatchers.IO).launch {
while (true) {
delay(config.syncInterval * 1000L)
try { fetchChanged() } catch (e: Exception) {
System.err.println("[ConfigClient] sync error: ${e.message}")
}
}
}
}
fun stop() { started = false }
private fun fetchAll() {
val url = "${config.baseUrl}/api/fetchAllConfigs"
val body = """{"appName":"${config.appName}","env":"${config.env}","version":"${config.version}","ignoreGlobal":${config.ignoreGlobal},"token":"${config.token}"}"""
val req = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json").timeout(Duration.ofSeconds(10)).build()
val resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString())
if (resp.statusCode() != 200) throw RuntimeException("HTTP ${resp.statusCode()}")
val data = json.parseToJsonElement(resp.body()).jsonObject
if (data.containsKey("code") && data["code"]!!.jsonPrimitive.int != 0)
throw RuntimeException("server error: ${data["code"]}")
data["latest.fetch.timestamp"]?.let { lastTime = it.jsonPrimitive.content.toLong() }
Cache(config.cacheDir).write(resp.body())
applyConfigs(resp.body())
System.err.println("[ConfigClient] fetched ${configs.size} configs")
}
private fun fetchChanged() {
val url = "${config.baseUrl}/api/fetchChangedConfigs"
val body = """{"appName":"${config.appName}","env":"${config.env}","version":"${config.version}","lastTime":$lastTime,"ignoreGlobal":${config.ignoreGlobal},"token":"${config.token}"}"""
val req = HttpRequest.newBuilder().uri(URI.create(url)).POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json").timeout(Duration.ofSeconds(10)).build()
val resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString())
val result = json.parseToJsonElement(resp.body()).jsonObject
if (!result.containsKey("code") || result["code"]!!.jsonPrimitive.int != 0)
throw RuntimeException("server error")
val changedData = result["data"]!!.jsonObject.toMutableMap()
changedData.remove("latest.fetch.timestamp")?.let { lastTime = it.jsonPrimitive.content.toLong() }
val changed = mutableMapOf<String, String>()
changedData.forEach { (k, v) -> changed[k] = v.jsonPrimitive.content }
configs.putAll(changed)
handlers.forEach { try { it(changed) } catch (e: Exception) { System.err.println("[ConfigClient] handler error: ${e.message}") } }
}
private fun applyConfigs(jsonStr: String) {
val data = json.parseToJsonElement(jsonStr).jsonObject
data.forEach { (k, v) ->
if (!k.startsWith("kenaito.") && k != "latest.fetch.timestamp")
configs[k] = v.jsonPrimitive.content
}
}
}
-49
View File
@@ -1,49 +0,0 @@
local ConfigClient = require("kenaito-conf")
local config_values = {
["database.host"] = os.getenv("DATABASE_HOST") or "192.168.1.1",
["database.port"] = os.getenv("DATABASE_PORT") or "8888",
["database.name"] = os.getenv("DATABASE_NAME") or "xxxx",
["database.user"] = os.getenv("DATABASE_USER") or "xxxx",
["database.password"] = os.getenv("DATABASE_PASSWORD") or "xxxxxxxx",
}
local config = {
baseUrl = os.getenv("KENAITO_BASE_URL") or "http://localhost:8080",
appName = os.getenv("KENAITO_APP_NAME") or "kenaito-springboot-demo",
env = os.getenv("KENAITO_PROFILE") or "daily",
version = os.getenv("KENAITO_VERSION") or "latest",
token = os.getenv("KENAITO_TOKEN") or "",
syncInterval = tonumber(os.getenv("KENAITO_SYNC_INTERVAL") or "10"),
cacheDir = os.getenv("KENAITO_CACHE_DIR") or ".",
}
local client = ConfigClient.new(config)
client:on_change(function(changed)
for k, v in pairs(changed) do
if config_values[k] then
config_values[k] = v
print("config updated: " .. k .. " = " .. v)
end
end
end)
client:start()
local remote = client:get_all()
for k, v in pairs(remote) do
if config_values[k] then
config_values[k] = v
end
end
print("Current configs:")
for k, v in pairs(config_values) do
print(" " .. k .. " = " .. v)
end
print("\nListening for config changes... (press Ctrl+C to stop)")
while true do
os.execute("sleep 1")
end
-73
View File
@@ -1,73 +0,0 @@
# Kenaito Config Center Lua Client
Lua 语言的配置中心客户端 SDK。
## 依赖
- Lua 5.3+
- LuaSocket
- lua-cjson
- lua-openssl
## 安装
```bash
luarocks install kenaito-conf-client-1.0.0-1.rockspec
```
或手动安装依赖:
```bash
luarocks install luasocket
luarocks install lua-cjson
luarocks install lua-openssl
```
## 使用
```lua
local ConfigClient = require("kenaito-conf")
local config = {
baseUrl = "http://localhost:8080",
appName = "kenaito-springboot-demo",
env = "daily",
version = "latest",
syncInterval = 30,
}
local client = ConfigClient.new(config)
client:on_change(function(changed)
for k, v in pairs(changed) do
print("config changed: " .. k .. " = " .. v)
end
end)
client:start()
local value = client:get("database.host")
local all = client:get_all()
client:stop()
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `baseUrl` | 配置中心地址 | 必填 |
| `appName` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `syncInterval` | 同步间隔(秒) | `60` |
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
| `cacheDir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-lua-client-demo
lua app.lua
```
-84
View File
@@ -1,84 +0,0 @@
local http = require("socket.http")
local ltn12 = require("ltn12")
local cjson = require("cjson")
local ConfigClient = {}
ConfigClient.__index = ConfigClient
function ConfigClient.new(config)
local self = setmetatable({}, ConfigClient)
self.config = config or {}
self.config.baseUrl = self.config.baseUrl or ""
self.config.appName = self.config.appName or ""
self.config.env = self.config.env or "dev"
self.config.version = self.config.version or "latest"
self.config.token = self.config.token or ""
self.config.syncInterval = self.config.syncInterval or 60
self.config.ignoreGlobal = self.config.ignoreGlobal or false
self.config.cacheDir = self.config.cacheDir or "."
self.configs = {}
self.lastTime = 0
self.handlers = {}
return self
end
function ConfigClient:on_change(handler)
table.insert(self.handlers, handler)
end
function ConfigClient:get(key)
return self.configs[key]
end
function ConfigClient:get_all()
return self.configs
end
function ConfigClient:start()
-- Fetch all configs
local url = self.config.baseUrl .. "/api/fetchAllConfigs"
local payload = cjson.encode({
appName = self.config.appName,
env = self.config.env,
version = self.config.version,
ignoreGlobal = self.config.ignoreGlobal,
token = self.config.token
})
local response_body = {}
local res, code, response_headers = http.request{
url = url,
method = "POST",
headers = {
["Content-Type"] = "application/json",
["Content-Length"] = #payload
},
source = ltn12.source.string(payload),
sink = ltn12.sink.table(response_body)
}
if code == 200 then
local data = cjson.decode(table.concat(response_body))
if data["latest.fetch.timestamp"] then
self.lastTime = tonumber(data["latest.fetch.timestamp"])
end
for k, v in pairs(data) do
if not string.match(k, "^kenaito%.") and k ~= "latest.fetch.timestamp" then
if type(v) == "string" then
self.configs[k] = v
end
end
end
print("[ConfigClient] fetched " .. #self.configs .. " configs")
end
end
function ConfigClient:stop()
-- Cleanup
end
return ConfigClient
@@ -1,22 +0,0 @@
package = "kenaito-conf-client"
version = "1.0.0-1"
source = {
url = "git+https://gitea.odboy.cn/odboy/kenaito-conf.git"
}
description = {
summary = "Kenaito Config Center Lua Client",
homepage = "https://gitea.odboy.cn/odboy/kenaito-conf",
license = "MIT"
}
dependencies = {
"lua >= 5.3",
"luasocket",
"lua-cjson",
"lua-openssl"
}
build = {
type = "builtin",
modules = {
["kenaito-conf"] = "client.lua"
}
}
-61
View File
@@ -1,61 +0,0 @@
<?php
require_once __DIR__ . '/../kenaito-conf-php-client/autoload.php';
use KenaitoConf\Config;
use KenaitoConf\ConfigClient;
$configValues = [
'database.host' => getenv('DATABASE_HOST') ?: '192.168.1.1',
'database.port' => getenv('DATABASE_PORT') ?: '8888',
'database.name' => getenv('DATABASE_NAME') ?: 'xxxx',
'database.user' => getenv('DATABASE_USER') ?: 'xxxx',
'database.password' => getenv('DATABASE_PASSWORD') ?: 'xxxxxxxx',
];
$config = new Config([
'baseUrl' => getenv('KENAITO_BASE_URL') ?: 'http://localhost:8080',
'appName' => getenv('KENAITO_APP_NAME') ?: 'kenaito-springboot-demo',
'env' => getenv('KENAITO_PROFILE') ?: 'daily',
'version' => getenv('KENAITO_VERSION') ?: 'latest',
'token' => getenv('KENAITO_TOKEN') ?: '',
'syncInterval' => (int)(getenv('KENAITO_SYNC_INTERVAL') ?: 10),
'cacheDir' => getenv('KENAITO_CACHE_DIR') ?: '.',
]);
$client = new ConfigClient($config);
$client->onChange(function ($changed) use (&$configValues) {
foreach ($changed as $k => $v) {
if (isset($configValues[$k])) {
$configValues[$k] = $v;
error_log("config updated: {$k} = {$v}");
}
}
});
try {
$client->start();
$remote = $client->getAll();
foreach ($configValues as $k => &$v) {
if (isset($remote[$k])) {
$v = $remote[$k];
}
}
error_log("loaded " . count($remote) . " configs from remote");
} catch (Exception $e) {
error_log("failed to init config client: " . $e->getMessage());
}
$port = (int)(getenv('SERVER_PORT') ?: 8084);
echo "Server started on port {$port}\n";
echo "Current configs:\n";
foreach ($configValues as $k => $v) {
echo " {$k} = {$v}\n";
}
while (true) {
pcntl_signal_dispatch();
usleep(100000);
}
-91
View File
@@ -1,91 +0,0 @@
# Kenaito Config Center PHP Client
PHP 语言的配置中心客户端 SDK。
## 依赖
- PHP 8.0+
- ext-curl
- ext-openssl
- ext-pcntl
## 安装
### Composer
```bash
cd kenaito-conf-php-client
composer install
```
或在项目中引用:
```json
{
"require": {
"kenaito/conf-client": "*"
},
"repositories": [
{
"type": "path",
"url": "../kenaito-conf-php-client"
}
]
}
```
### 手动引入
```php
require_once 'kenaito-conf-php-client/autoload.php';
```
## 使用
```php
use KenaitoConf\Config;
use KenaitoConf\ConfigClient;
$config = new Config([
'baseUrl' => 'http://localhost:8080',
'appName' => 'kenaito-springboot-demo',
'env' => 'daily',
'version' => 'latest',
'syncInterval' => 30,
]);
$client = new ConfigClient($config);
$client->onChange(function ($changed) {
foreach ($changed as $k => $v) {
echo "config changed: $k = $v\n";
}
});
$client->start();
$value = $client->get('database.host');
$all = $client->getAll();
$client->stop();
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `baseUrl` | 配置中心地址 | 必填 |
| `appName` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `syncInterval` | 同步间隔(秒) | `60` |
| `ignoreGlobal` | 是否忽略全局配置 | `false` |
| `cacheDir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-php-client-demo
php app.php
```
-6
View File
@@ -1,6 +0,0 @@
<?php
require_once __DIR__ . '/src/Config.php';
require_once __DIR__ . '/src/Crypto.php';
require_once __DIR__ . '/src/Cache.php';
require_once __DIR__ . '/src/ConfigClient.php';
-16
View File
@@ -1,16 +0,0 @@
{
"name": "kenaito/conf-client",
"description": "Kenaito Config Center PHP Client",
"type": "library",
"require": {
"php": ">=8.0",
"ext-curl": "*",
"ext-openssl": "*",
"ext-pcntl": "*"
},
"autoload": {
"psr-4": {
"KenaitoConf\\": "src/"
}
}
}
-30
View File
@@ -1,30 +0,0 @@
<?php
namespace KenaitoConf;
class Cache
{
private string $cacheDir;
private const CACHE_FILE = 'config-cache.json';
public function __construct(string $cacheDir = '.')
{
$this->cacheDir = $cacheDir;
}
public function write(array $data): void
{
$path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE;
file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
public function read(): ?array
{
$path = $this->cacheDir . DIRECTORY_SEPARATOR . self::CACHE_FILE;
if (!file_exists($path)) {
return null;
}
$content = file_get_contents($path);
return json_decode($content, true);
}
}
-24
View File
@@ -1,24 +0,0 @@
<?php
namespace KenaitoConf;
class Config
{
public string $baseUrl = '';
public string $appName = '';
public string $env = 'dev';
public string $version = 'latest';
public string $token = '';
public int $syncInterval = 60;
public bool $ignoreGlobal = false;
public string $cacheDir = '.';
public function __construct(array $options = [])
{
foreach ($options as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
}
@@ -1,179 +0,0 @@
<?php
namespace KenaitoConf;
class ConfigClient
{
private Config $config;
private Cache $cache;
private array $configs = [];
private int $lastTime = 0;
private bool $started = false;
private array $handlers = [];
private $timer = null;
public function __construct(Config $config)
{
$this->config = $config;
$this->cache = new Cache($config->cacheDir);
}
public function onChange(callable $handler): void
{
$this->handlers[] = $handler;
}
public function get(string $key): ?string
{
return $this->configs[$key] ?? null;
}
public function getAll(): array
{
return $this->configs;
}
public function start(): void
{
if ($this->started) {
return;
}
$this->started = true;
try {
$this->fetchAll();
} catch (\Exception $e) {
error_log("[ConfigClient] fetch all configs error: {$e->getMessage()}, trying cache");
$cached = $this->cache->read();
if ($cached !== null) {
$this->applyConfigs($cached);
error_log("[ConfigClient] loaded " . count($this->configs) . " configs from cache");
} else {
throw new \RuntimeException("fetch configs failed and no cache: {$e->getMessage()}");
}
}
$this->timer = pcntl_alarm(0);
pcntl_signal(SIGALRM, [$this, 'syncTick']);
pcntl_alarm($this->config->syncInterval);
}
public function syncTick(): void
{
try {
$this->fetchChanged();
} catch (\Exception $e) {
error_log("[ConfigClient] sync error: {$e->getMessage()}");
}
pcntl_alarm($this->config->syncInterval);
}
public function stop(): void
{
if ($this->timer !== null) {
pcntl_alarm(0);
}
}
private function fetchAll(): void
{
$url = $this->config->baseUrl . '/api/fetchAllConfigs';
$payload = [
'appName' => $this->config->appName,
'env' => $this->config->env,
'version' => $this->config->version,
'ignoreGlobal' => $this->config->ignoreGlobal,
'token' => $this->config->token,
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \RuntimeException("HTTP {$httpCode}");
}
$data = json_decode($response, true);
if (isset($data['code']) && $data['code'] !== 0) {
throw new \RuntimeException("server error: " . ($data['msg'] ?? 'unknown'));
}
if (isset($data['latest.fetch.timestamp'])) {
$this->lastTime = (int)$data['latest.fetch.timestamp'];
}
$this->cache->write($data);
$this->applyConfigs($data);
error_log("[ConfigClient] fetched " . count($this->configs) . " configs");
}
private function fetchChanged(): void
{
$url = $this->config->baseUrl . '/api/fetchChangedConfigs';
$payload = [
'appName' => $this->config->appName,
'env' => $this->config->env,
'version' => $this->config->version,
'lastTime' => $this->lastTime,
'ignoreGlobal' => $this->config->ignoreGlobal,
'token' => $this->config->token,
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
if (!isset($result['code']) || $result['code'] !== 0) {
throw new \RuntimeException("server error code: " . ($result['code'] ?? 'unknown'));
}
$changedData = $result['data'] ?? [];
if (isset($changedData['latest.fetch.timestamp'])) {
$this->lastTime = (int)$changedData['latest.fetch.timestamp'];
unset($changedData['latest.fetch.timestamp']);
}
$changed = [];
foreach ($changedData as $k => $v) {
$changed[$k] = $v;
}
$this->configs = array_merge($this->configs, $changed);
foreach ($this->handlers as $handler) {
try {
$handler($changed);
} catch (\Exception $e) {
error_log("[ConfigClient] change handler error: {$e->getMessage()}");
}
}
error_log("[ConfigClient] " . count($changed) . " configs changed");
}
private function applyConfigs(array $data): void
{
foreach ($data as $k => $v) {
if (str_starts_with($k, 'kenaito.') || $k === 'latest.fetch.timestamp') {
continue;
}
if (!is_string($v)) {
continue;
}
$this->configs[$k] = $v;
}
}
}
-51
View File
@@ -1,51 +0,0 @@
# frozen_string_literal: true
require_relative '../kenaito-conf-ruby-client/lib/kenaito_conf'
config_values = {
'database.host' => ENV['DATABASE_HOST'] || '192.168.1.1',
'database.port' => ENV['DATABASE_PORT'] || '8888',
'database.name' => ENV['DATABASE_NAME'] || 'xxxx',
'database.user' => ENV['DATABASE_USER'] || 'xxxx',
'database.password' => ENV['DATABASE_PASSWORD'] || 'xxxxxxxx'
}
config = KenaitoConf::Config.new(
base_url: ENV['KENAITO_BASE_URL'] || 'http://localhost:8080',
app_name: ENV['KENAITO_APP_NAME'] || 'kenaito-springboot-demo',
env: ENV['KENAITO_PROFILE'] || 'daily',
version: ENV['KENAITO_VERSION'] || 'latest',
token: ENV['KENAITO_TOKEN'] || '',
sync_interval: (ENV['KENAITO_SYNC_INTERVAL'] || '30').to_i,
cache_dir: ENV['KENAITO_CACHE_DIR'] || '.'
)
client = KenaitoConf::ConfigClient.new(config)
client.on_change do |changed|
changed.each do |k, v|
if config_values.key?(k)
config_values[k] = v
warn "config updated: #{k} = #{v}"
end
end
end
begin
client.start
remote = client.get_all
config_values.each_key do |k|
config_values[k] = remote[k] if remote[k]
end
warn "loaded #{remote.size} configs from remote"
rescue StandardError => e
warn "failed to init config client: #{e.message}"
end
puts 'Current configs:'
config_values.each do |k, v|
puts " #{k} = #{v}"
end
puts "\nListening for config changes... (press Ctrl+C to stop)"
sleep
-70
View File
@@ -1,70 +0,0 @@
# Kenaito Config Center Ruby Client
Ruby 语言的配置中心客户端 SDK。
## 依赖
- Ruby 2.7+
- 标准库(net/http, json, openssl, base64)
## 安装
```bash
gem build kenaito-conf.gemspec
gem install kenaito-conf-1.0.0.gem
```
或在 Gemfile 中添加:
```ruby
gem 'kenaito-conf', path: '../kenaito-conf-ruby-client'
```
## 使用
```ruby
require 'kenaito_conf'
config = KenaitoConf::Config.new(
base_url: 'http://localhost:8080',
app_name: 'kenaito-springboot-demo',
env: 'daily',
version: 'latest',
sync_interval: 30
)
client = KenaitoConf::ConfigClient.new(config)
client.on_change do |changed|
changed.each do |k, v|
puts "config changed: #{k} = #{v}"
end
end
client.start
value = client.get('database.host')
all = client.get_all
client.stop
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `base_url` | 配置中心地址 | 必填 |
| `app_name` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `sync_interval` | 同步间隔(秒) | `60` |
| `ignore_global` | 是否忽略全局配置 | `false` |
| `cache_dir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-ruby-client-demo
ruby app.rb
```
@@ -1,18 +0,0 @@
# frozen_string_literal: true
Gem::Specification.new do |spec|
spec.name = 'kenaito-conf'
spec.version = '1.0.0'
spec.authors = ['odboy']
spec.email = ['tianjun@odboy.cn']
spec.summary = 'Kenaito Config Center Ruby Client'
spec.description = 'Ruby client for Kenaito Config Center'
spec.homepage = 'https://gitea.odboy.cn/odboy/kenaito-conf'
spec.license = 'MIT'
spec.required_ruby_version = '>= 2.7.0'
spec.files = Dir['lib/**/*']
spec.require_paths = ['lib']
end
@@ -1,165 +0,0 @@
# frozen_string_literal: true
require 'net/http'
require 'json'
module KenaitoConf
class Config
attr_accessor :base_url, :app_name, :env, :version, :token, :sync_interval, :ignore_global, :cache_dir
def initialize(options = {})
@base_url = options[:base_url] || ''
@app_name = options[:app_name] || ''
@env = options[:env] || 'dev'
@version = options[:version] || 'latest'
@token = options[:token] || ''
@sync_interval = options[:sync_interval] || 60
@ignore_global = options[:ignore_global] || false
@cache_dir = options[:cache_dir] || '.'
end
end
class Cache
CACHE_FILE = 'config-cache.json'
def initialize(cache_dir = '.')
@cache_dir = cache_dir
end
def write(data)
File.write(File.join(@cache_dir, CACHE_FILE), JSON.pretty_generate(data))
end
def read
path = File.join(@cache_dir, CACHE_FILE)
return nil unless File.exist?(path)
JSON.parse(File.read(path))
rescue StandardError
nil
end
end
class ConfigClient
def initialize(config)
@config = config
@cache = Cache.new(config.cache_dir)
@configs = {}
@last_time = 0
@handlers = []
@started = false
end
def on_change(&handler)
@handlers << handler
end
def get(key)
@configs[key]
end
def get_all
@configs.dup
end
def start
return if @started
@started = true
begin
fetch_all
rescue StandardError => e
warn "[ConfigClient] fetch all configs error: #{e.message}, trying cache"
cached = @cache.read
if cached
apply_configs(cached)
warn "[ConfigClient] loaded #{@configs.size} configs from cache"
else
raise "fetch configs failed and no cache: #{e.message}"
end
end
Thread.new do
loop do
sleep @config.sync_interval
begin
fetch_changed
rescue StandardError => e
warn "[ConfigClient] sync error: #{e.message}"
end
end
end
end
def stop
@started = false
end
private
def fetch_all
uri = URI("#{@config.base_url}/api/fetchAllConfigs")
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 10
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
request.body = JSON.generate({
appName: @config.app_name,
env: @config.env,
version: @config.version,
ignoreGlobal: @config.ignore_global,
token: @config.token
})
response = http.request(request)
raise "HTTP #{response.code}" unless response.code == '200'
data = JSON.parse(response.body)
raise "server error: #{data['msg']}" if data['code'] && data['code'] != 0
@last_time = data['latest.fetch.timestamp'].to_i if data['latest.fetch.timestamp']
@cache.write(data)
apply_configs(data)
warn "[ConfigClient] fetched #{@configs.size} configs"
end
def fetch_changed
uri = URI("#{@config.base_url}/api/fetchChangedConfigs")
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 10
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
request.body = JSON.generate({
appName: @config.app_name,
env: @config.env,
version: @config.version,
lastTime: @last_time,
ignoreGlobal: @config.ignore_global,
token: @config.token
})
response = http.request(request)
result = JSON.parse(response.body)
raise "server error" if !result['code'] || result['code'] != 0
changed_data = result['data'] || {}
@last_time = changed_data.delete('latest.fetch.timestamp').to_i if changed_data['latest.fetch.timestamp']
changed = {}
changed_data.each do |k, v|
changed[k] = v
end
@configs.merge!(changed)
@handlers.each { |h| h.call(changed) rescue warn "[ConfigClient] handler error: #{$!.message}" }
end
def apply_configs(data)
data.each do |k, v|
next if k.start_with?('kenaito.') || k == 'latest.fetch.timestamp'
next unless v.is_a?(String)
@configs[k] = v
end
end
end
end
-8
View File
@@ -1,8 +0,0 @@
[package]
name = "kenaito-conf-demo"
version = "1.0.0"
edition = "2021"
[dependencies]
kenaito-conf = { path = "../kenaito-conf-rust-client" }
tokio = { version = "1", features = ["full"] }
-52
View File
@@ -1,52 +0,0 @@
use kenaito_conf::{Config, ConfigClient};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config_values: HashMap<String, String> = HashMap::new();
config_values.insert("database.host".to_string(), std::env::var("DATABASE_HOST").unwrap_or_else(|_| "192.168.1.1".to_string()));
config_values.insert("database.port".to_string(), std::env::var("DATABASE_PORT").unwrap_or_else(|_| "8888".to_string()));
config_values.insert("database.name".to_string(), std::env::var("DATABASE_NAME").unwrap_or_else(|_| "xxxx".to_string()));
config_values.insert("database.user".to_string(), std::env::var("DATABASE_USER").unwrap_or_else(|_| "xxxx".to_string()));
config_values.insert("database.password".to_string(), std::env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "xxxxxxxx".to_string()));
let config = Config {
base_url: std::env::var("KENAITO_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()),
app_name: std::env::var("KENAITO_APP_NAME").unwrap_or_else(|_| "kenaito-springboot-demo".to_string()),
env: std::env::var("KENAITO_PROFILE").unwrap_or_else(|_| "daily".to_string()),
version: std::env::var("KENAITO_VERSION").unwrap_or_else(|_| "latest".to_string()),
token: std::env::var("KENAITO_TOKEN").unwrap_or_else(|_| String::new()),
sync_interval: std::env::var("KENAITO_SYNC_INTERVAL").unwrap_or_else(|_| "30".to_string()).parse().unwrap_or(30),
cache_dir: std::env::var("KENAITO_CACHE_DIR").unwrap_or_else(|_| ".".to_string()),
..Default::default()
};
let client = ConfigClient::new(config);
client.on_change(move |changed| {
for (k, v) in changed.iter() {
eprintln!("config changed: {} = {}", k, v);
}
});
match client.start().await {
Ok(_) => {
let remote = client.get_all();
eprintln!("loaded {} configs from remote", remote.len());
}
Err(e) => {
eprintln!("failed to init config client: {}", e);
}
}
println!("Current configs:");
for (k, v) in &config_values {
println!(" {} = {}", k, v);
}
println!("\nListening for config changes... (press Ctrl+C to stop)");
tokio::signal::ctrl_c().await?;
println!("Shutting down...");
Ok(())
}
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "kenaito-conf"
version = "1.0.0"
edition = "2021"
authors = ["odboy <tianjun@odboy.cn>"]
description = "Kenaito Config Center Rust Client"
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
md-5 = "0.10"
aes = "0.8"
cbc = "0.1"
base64 = "0.21"
-70
View File
@@ -1,70 +0,0 @@
# Kenaito Config Center Rust Client
Rust 语言的配置中心客户端 SDK。
## 依赖
- Rust 1.70+
- reqwest (HTTP 客户端)
- tokio (异步运行时)
- aes + cbc (AES 解密)
## 安装
在 `Cargo.toml` 中添加:
```toml
[dependencies]
kenaito-conf = { path = "../kenaito-conf-rust-client" }
```
## 使用
```rust
use kenaito_conf::{Config, ConfigClient};
#[tokio::main]
async fn main() {
let config = Config {
base_url: "http://localhost:8080".to_string(),
app_name: "kenaito-springboot-demo".to_string(),
env: "daily".to_string(),
version: "latest".to_string(),
sync_interval: 30,
..Default::default()
};
let client = ConfigClient::new(config);
client.on_change(|changed| {
for (k, v) in changed.iter() {
println!("config changed: {} = {}", k, v);
}
});
client.start().await.unwrap();
let value = client.get("database.host");
let all = client.get_all();
}
```
## 配置参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `base_url` | 配置中心地址 | 必填 |
| `app_name` | 应用名称 | 必填 |
| `env` | 环境 | `dev` |
| `version` | 配置版本 | `latest` |
| `token` | 认证令牌 | 空 |
| `sync_interval` | 同步间隔(秒) | `60` |
| `ignore_global` | 是否忽略全局配置 | `false` |
| `cache_dir` | 本地缓存目录 | `.` |
## Demo
```bash
cd kenaito-conf-rust-client-demo
cargo run
```
-140
View File
@@ -1,140 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
#[derive(Clone, Debug)]
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: String::new(),
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(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
struct FetchAllRequest {
app_name: String,
env: String,
version: String,
ignore_global: bool,
token: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct FetchChangedRequest {
app_name: String,
env: String,
version: String,
last_time: i64,
ignore_global: bool,
token: String,
}
pub struct ConfigClient {
config: Config,
configs: Arc<Mutex<HashMap<String, String>>>,
last_time: Arc<Mutex<i64>>,
handlers: Arc<Mutex<Vec<Box<dyn Fn(&HashMap<String, String>) + Send + Sync>>>>,
}
impl ConfigClient {
pub fn new(config: Config) -> Self {
Self {
config,
configs: Arc::new(Mutex::new(HashMap::new())),
last_time: Arc::new(Mutex::new(0)),
handlers: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn on_change<F>(&self, handler: F)
where
F: Fn(&HashMap<String, String>) + Send + Sync + 'static,
{
self.handlers.lock().unwrap().push(Box::new(handler));
}
pub fn get(&self, key: &str) -> Option<String> {
self.configs.lock().unwrap().get(key).cloned()
}
pub fn get_all(&self) -> HashMap<String, String> {
self.configs.lock().unwrap().clone()
}
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error>> {
self.fetch_all().await?;
let configs = self.configs.clone();
let last_time = self.last_time.clone();
let handlers = self.handlers.clone();
let config = self.config.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(config.sync_interval)).await;
// fetch_changed logic here
}
});
Ok(())
}
async fn fetch_all(&self) -> Result<(), Box<dyn std::error::Error>> {
let url = format!("{}/api/fetchAllConfigs", self.config.base_url);
let client = reqwest::Client::new();
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 = client.post(&url).json(&payload).send().await?;
let data: HashMap<String, serde_json::Value> = resp.json().await?;
if let Some(code) = data.get("code") {
if code.as_i64() != Some(0) {
return Err("server error".into());
}
}
if let Some(ts) = data.get("latest.fetch.timestamp") {
*self.last_time.lock().unwrap() = ts.as_i64().unwrap_or(0);
}
let mut configs = self.configs.lock().unwrap();
for (k, v) in data.iter() {
if k.starts_with("kenaito.") || k == "latest.fetch.timestamp" {
continue;
}
if let Some(s) = v.as_str() {
configs.insert(k.clone(), s.to_string());
}
}
Ok(())
}
}