feat: 新增 8 个语言客户端 SDK
- PHP 客户端 (kenaito-conf-php-client) - .NET 客户端 (kenaito-conf-dotnet-client) - C 客户端 (kenaito-conf-c-client) - C++ 客户端 (kenaito-conf-cpp-client) - Lua 客户端 (kenaito-conf-lua-client) - Kotlin 客户端 (kenaito-conf-kotlin-client) - Rust 客户端 (kenaito-conf-rust-client) - Ruby 客户端 (kenaito-conf-ruby-client) 每个客户端包含完整的 SDK 实现和 demo 示例,支持: - 配置拉取(全量/增量) - AES-128-CBC 解密 - 本地缓存 - 变更回调 - Token 认证 更新根目录 README.md: - 技术栈列表新增 8 种语言 - 项目结构新增 16 个目录 - 客户端接入章节新增 8 个使用示例
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
<?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';
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace KenaitoConf;
|
||||
|
||||
class ConfigClient
|
||||
{
|
||||
private Config $config;
|
||||
private Cache $cache;
|
||||
private array $configs = [];
|
||||
private string $secret = '';
|
||||
private string $globalSecret = '';
|
||||
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['kenaito.config.encrypt-secret'])) {
|
||||
$this->secret = $data['kenaito.config.encrypt-secret'];
|
||||
}
|
||||
if (isset($data['kenaito.config.global-encrypt-secret'])) {
|
||||
$this->globalSecret = $data['kenaito.config.global-encrypt-secret'];
|
||||
}
|
||||
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] = $this->tryDecrypt($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] = $this->tryDecrypt($v);
|
||||
}
|
||||
}
|
||||
|
||||
private function tryDecrypt(string $value): string
|
||||
{
|
||||
$decrypted = Crypto::decryptConfig($this->secret, $value);
|
||||
if ($decrypted === $value && $this->globalSecret) {
|
||||
$decrypted = Crypto::decryptConfig($this->globalSecret, $value);
|
||||
}
|
||||
return $decrypted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace KenaitoConf;
|
||||
|
||||
class Crypto
|
||||
{
|
||||
private const CRYPT_PREFIX = '{Cipher}';
|
||||
|
||||
public static function decryptConfig(string $secret, string $data): string
|
||||
{
|
||||
if (!str_starts_with($data, self::CRYPT_PREFIX)) {
|
||||
return $data;
|
||||
}
|
||||
$encrypted = substr($data, strlen(self::CRYPT_PREFIX));
|
||||
return self::decryptWithAES($secret, $encrypted);
|
||||
}
|
||||
|
||||
public static function decryptWithAES(string $key, string $data): string
|
||||
{
|
||||
$md5Hash = md5($key);
|
||||
$aesKey = substr($md5Hash, 16);
|
||||
$cipherText = base64_decode($data);
|
||||
$iv = str_repeat("\0", 16);
|
||||
$plainText = openssl_decrypt($cipherText, 'AES-128-CBC', $aesKey, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
|
||||
$padding = ord($plainText[strlen($plainText) - 1]);
|
||||
return substr($plainText, 0, -$padding);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user