feat(c-client): 实现完整的 C 客户端 SDK

核心功能:
- JSON 解析:使用 cJSON 解析服务端返回的配置
- 配置缓存:自动保存到本地 .kenaito-config-cache.json 文件
- 定时同步:后台线程定时拉取配置变更(默认 10 秒)
- 配置变更回调:配置变更时自动触发回调函数
- 全量/增量拉取:首次全量拉取,后续通过 lastTime 增量拉取
- 优雅退出:支持信号处理,Ctrl+C 优雅关闭

API:
- config_client_init() - 初始化客户端
- config_client_start() - 启动客户端(全量拉取 + 后台同步)
- config_client_get() - 获取配置值
- config_client_stop() - 停止客户端并保存缓存

依赖:
- libcurl(HTTP 请求)
- cJSON(JSON 解析)
- pthread(后台同步线程)

编译:
- SDK: gcc -c main.c -o kenaito_conf.o -lcurl -lcjson
- Demo: gcc -o demo main.c ../kenaito-conf-c-client/main.c -lcurl -lcjson -lpthread
This commit is contained in:
2026-04-15 21:02:16 +08:00
parent 102c62b64b
commit 7bf1dff3ce
3 changed files with 531 additions and 78 deletions
+54 -12
View File
@@ -22,22 +22,66 @@ yum install libcurl-devel
## 快速开始
### 1. 编译示例
### 1. 安装依赖
#### Ubuntu/Debian
```bash
apt-get install libcurl4-openssl-dev libcjson-dev
```
#### CentOS/RHEL
```bash
yum install libcurl-devel libcjson-devel
```
#### macOS
```bash
brew install cJSON
```
> **macOS**:系统自带 libcurl,无需额外安装。
### 2. 编译 SDK
```bash
cd kenaito-conf-c-client
gcc -c main.c -o kenaito_conf.o -lcurl -lcjson
ar rcs libkenaito_conf.a kenaito_conf.o
```
### 3. 编译 Demo
```bash
cd kenaito-conf-c-client-demo
gcc -o demo ../kenaito-conf-c-client/main.c -lcurl
gcc -o demo main.c ../kenaito-conf-c-client/main.c -lcurl -lcjson -lpthread
./demo
```
### 2. 使用示例
### 4. 使用 SDK
在你的项目中:
```c
#include "kenaito-conf-c-client/main.c"
// 或者链接静态库
// gcc -o myapp myapp.c -lkenaito_conf -lcurl -lcjson -lpthread
```
### 5. 完整示例
```c
#include <stdio.h>
#include "main.c"
#include "../kenaito-conf-c-client/main.c"
void on_change(const char *key, const char *value) {
printf("config changed: %s = %s\n", key, value);
}
int main() {
// 配置客户端参数
Config config = {
.baseUrl = "http://localhost:8080",
.appName = "my-app",
@@ -51,15 +95,10 @@ int main() {
ConfigClient client;
config_client_init(&client, &config);
// 注册配置变更回调
client.onChange = [](const char *key, const char *value) {
printf("config changed: %s = %s\n", key, value);
};
client.onChange = on_change;
// 启动客户端(拉取全量配置 + 开启定时同步)
// 注:当前版本为简化实现,仅演示单次拉取
// 完整版本需实现定时轮询和长轮询机制
config_client_start(&client);
// 获取配置
const char *dbHost = config_client_get(&client, "database.host");
@@ -68,6 +107,9 @@ int main() {
printf("database.host = %s\n", dbHost ? dbHost : "not found");
printf("database.port = %s\n", dbPort ? dbPort : "not found");
// 等待配置变更
sleep(30);
// 停止客户端
config_client_stop(&client);
return 0;
+363 -63
View File
@@ -1,13 +1,41 @@
/**
* Kenaito Config Center C Client SDK
*
* 完整的配置中心客户端,支持:
* - JSON 解析(cJSON)
* - 配置缓存到本地文件
* - 定时同步配置变更
* - 配置变更回调
* - 全量/增量拉取
*
* 依赖:
* - libcurl
* - cJSON
* - pthread
*
* 编译:
* gcc -c main.c -o kenaito_conf.o -lcurl -lcjson
*
* @author odboy
* @version 1.0.0
* @date 2026-10-16
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <cjson/cJSON.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#define MAX_CONFIGS 256
#define MAX_KEY_LEN 256
#define MAX_VAL_LEN 4096
#define CACHE_FILE ".kenaito-config-cache.json"
/* ============ 配置结构 ============ */
typedef struct {
char baseUrl[512];
char appName[128];
@@ -19,28 +47,41 @@ typedef struct {
char cacheDir[256];
} Config;
/* ============ 配置条目 ============ */
typedef struct {
char key[MAX_KEY_LEN];
char value[MAX_VAL_LEN];
} ConfigEntry;
/* ============ 配置映射表 ============ */
typedef struct {
ConfigEntry entries[MAX_CONFIGS];
int count;
} ConfigMap;
/* ============ 客户端实例 ============ */
typedef struct {
Config config;
ConfigMap configs;
long lastTime;
void (*onChange)(const char *key, const char *value);
CURL *curl;
int running;
pthread_t syncThread;
} ConfigClient;
/* ============ HTTP 响应缓冲区 ============ */
typedef struct {
char *data;
size_t size;
} ResponseBuffer;
/* 全局客户端实例 */
static ConfigClient *g_client = NULL;
/* ============ 工具函数 ============ */
/* HTTP 响应回调 */
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t totalSize = size * nmemb;
ResponseBuffer *buf = (ResponseBuffer *)userp;
@@ -53,40 +94,117 @@ static size_t write_callback(void *contents, size_t size, size_t nmemb, void *us
return totalSize;
}
static char *http_post_json(const char *url, const char *jsonBody) {
CURL *curl = curl_easy_init();
if (!curl) return NULL;
/* HTTP POST 请求 */
static char *http_post(ConfigClient *client, const char *url, const char *jsonBody) {
ResponseBuffer buf = {0};
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
CURLcode res = curl_easy_perform(curl);
headers = curl_slist_append(headers, "Expect:");
curl_easy_setopt(client->curl, CURLOPT_URL, url);
curl_easy_setopt(client->curl, CURLOPT_POSTFIELDS, jsonBody);
curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, &buf);
curl_easy_setopt(client->curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(client->curl, CURLOPT_FOLLOWLOCATION, 1L);
CURLcode res = curl_easy_perform(client->curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
fprintf(stderr, "HTTP request failed: %s\n", curl_easy_strerror(res));
free(buf.data);
return NULL;
}
long httpCode = 0;
curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE, &httpCode);
if (httpCode != 200) {
fprintf(stderr, "HTTP %ld: %s\n", httpCode, buf.data ? buf.data : "empty response");
free(buf.data);
return NULL;
}
return buf.data;
}
void config_client_init(ConfigClient *client, Config *config) {
client->config = *config;
client->configs.count = 0;
client->lastTime = 0;
client->onChange = NULL;
curl_global_init(CURL_GLOBAL_DEFAULT);
/* 从缓存加载配置 */
static int load_from_cache(ConfigClient *client) {
FILE *f = fopen(CACHE_FILE, "r");
if (!f) return 0;
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
char *jsonStr = malloc(fsize + 1);
fread(jsonStr, 1, fsize, f);
fclose(f);
jsonStr[fsize] = 0;
cJSON *root = cJSON_Parse(jsonStr);
free(jsonStr);
if (!root) return 0;
cJSON *lastTimeObj = cJSON_GetObjectItem(root, "lastTime");
if (lastTimeObj) {
client->lastTime = lastTimeObj->valueint;
}
cJSON *configsObj = cJSON_GetObjectItem(root, "configs");
if (configsObj && cJSON_IsArray(configsObj)) {
client->configs.count = 0;
int arrSize = cJSON_GetArraySize(configsObj);
for (int i = 0; i < arrSize && client->configs.count < MAX_CONFIGS; i++) {
cJSON *item = cJSON_GetArrayItem(configsObj, i);
cJSON *keyObj = cJSON_GetObjectItem(item, "key");
cJSON *valObj = cJSON_GetObjectItem(item, "value");
if (keyObj && valObj && cJSON_IsString(keyObj) && cJSON_IsString(valObj)) {
strncpy(client->configs.entries[client->configs.count].key,
keyObj->valuestring, MAX_KEY_LEN - 1);
strncpy(client->configs.entries[client->configs.count].value,
valObj->valuestring, MAX_VAL_LEN - 1);
client->configs.count++;
}
}
}
cJSON_Delete(root);
printf("[Cache] Loaded %d configs from %s\n", client->configs.count, CACHE_FILE);
return 1;
}
/* 保存配置到缓存 */
static void save_to_cache(ConfigClient *client) {
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "lastTime", client->lastTime);
cJSON *configsArr = cJSON_CreateArray();
for (int i = 0; i < client->configs.count; i++) {
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "key", client->configs.entries[i].key);
cJSON_AddStringToObject(item, "value", client->configs.entries[i].value);
cJSON_AddItemToArray(configsArr, item);
}
cJSON_AddItemToObject(root, "configs", configsArr);
char *jsonStr = cJSON_PrintUnformatted(root);
FILE *f = fopen(CACHE_FILE, "w");
if (f) {
fprintf(f, "%s", jsonStr);
fclose(f);
printf("[Cache] Saved %d configs to %s\n", client->configs.count, CACHE_FILE);
}
free(jsonStr);
cJSON_Delete(root);
}
/* 获取配置值 */
const char *config_client_get(ConfigClient *client, const char *key) {
for (int i = 0; i < client->configs.count; i++) {
if (strcmp(client->configs.entries[i].key, key) == 0)
@@ -95,48 +213,230 @@ const char *config_client_get(ConfigClient *client, const char *key) {
return NULL;
}
void config_client_stop(ConfigClient *client) {
curl_global_cleanup();
}
int main() {
printf("Kenaito Config Center C Client Demo\n");
printf("AppName: kenaito-springboot-demo, Env: daily\n");
Config config = {
.baseUrl = "http://localhost:8080",
.appName = "kenaito-springboot-demo",
.env = "daily",
.version = "latest",
.token = "",
.syncInterval = 30,
.ignoreGlobal = 0,
.cacheDir = "."
};
ConfigClient client;
config_client_init(&client, &config);
/* Build fetchAllConfigs request */
char url[1024];
snprintf(url, sizeof(url), "%s/api/fetchAllConfigs", config.baseUrl);
char payload[512];
snprintf(payload, sizeof(payload),
"{\"appName\":\"%s\",\"env\":\"%s\",\"version\":\"%s\",\"ignoreGlobal\":false,\"token\":\"%s\"}",
config.appName, config.env, config.version, config.token);
printf("Fetching configs from %s ...\n", url);
char *response = http_post_json(url, payload);
if (response) {
printf("Response received (%zu bytes)\n", strlen(response));
/* In production: parse JSON, store configs */
printf("First 200 chars: %.200s\n", response);
free(response);
} else {
printf("Failed to fetch configs (server may not be running)\n");
/* 解析 JSON 响应 */
static int parse_json_response(const char *jsonStr, ConfigClient *client) {
cJSON *root = cJSON_Parse(jsonStr);
if (!root) {
fprintf(stderr, "Failed to parse JSON: %s\n", cJSON_GetErrorPtr());
return -1;
}
config_client_stop(&client);
cJSON *successObj = cJSON_GetObjectItem(root, "success");
if (successObj && cJSON_IsBool(successObj) && !successObj->valueint) {
cJSON *msgObj = cJSON_GetObjectItem(root, "message");
fprintf(stderr, "Server error: %s\n", msgObj ? msgObj->valuestring : "unknown");
cJSON_Delete(root);
return -1;
}
cJSON *dataObj = cJSON_GetObjectItem(root, "data");
if (!dataObj) {
fprintf(stderr, "No data in response\n");
cJSON_Delete(root);
return -1;
}
/* 全量配置 */
cJSON *configsObj = cJSON_GetObjectItem(dataObj, "configs");
if (configsObj && cJSON_IsArray(configsObj)) {
client->configs.count = 0;
int arrSize = cJSON_GetArraySize(configsObj);
for (int i = 0; i < arrSize && client->configs.count < MAX_CONFIGS; i++) {
cJSON *item = cJSON_GetArrayItem(configsObj, i);
cJSON *keyObj = cJSON_GetObjectItem(item, "key");
cJSON *valObj = cJSON_GetObjectItem(item, "value");
if (keyObj && valObj && cJSON_IsString(keyObj) && cJSON_IsString(valObj)) {
strncpy(client->configs.entries[client->configs.count].key,
keyObj->valuestring, MAX_KEY_LEN - 1);
const char *oldVal = config_client_get(client, keyObj->valuestring);
strncpy(client->configs.entries[client->configs.count].value,
valObj->valuestring, MAX_VAL_LEN - 1);
/* 检测变更并触发回调 */
if (client->onChange && oldVal && strcmp(oldVal, valObj->valuestring) != 0) {
printf("[Change] %s = %s -> %s\n",
keyObj->valuestring, oldVal, valObj->valuestring);
client->onChange(keyObj->valuestring, valObj->valuestring);
}
client->configs.count++;
}
}
}
/* 增量变更 */
cJSON *changesObj = cJSON_GetObjectItem(dataObj, "changes");
if (changesObj && cJSON_IsArray(changesObj)) {
int arrSize = cJSON_GetArraySize(changesObj);
for (int i = 0; i < arrSize; i++) {
cJSON *item = cJSON_GetArrayItem(changesObj, i);
cJSON *keyObj = cJSON_GetObjectItem(item, "key");
cJSON *valObj = cJSON_GetObjectItem(item, "value");
if (keyObj && valObj && cJSON_IsString(keyObj) && cJSON_IsString(valObj)) {
/* 更新或添加配置 */
int found = 0;
for (int j = 0; j < client->configs.count; j++) {
if (strcmp(client->configs.entries[j].key, keyObj->valuestring) == 0) {
const char *oldVal = client->configs.entries[j].value;
strncpy(client->configs.entries[j].value,
valObj->valuestring, MAX_VAL_LEN - 1);
if (client->onChange && strcmp(oldVal, valObj->valuestring) != 0) {
printf("[Change] %s = %s -> %s\n",
keyObj->valuestring, oldVal, valObj->valuestring);
client->onChange(keyObj->valuestring, valObj->valuestring);
}
found = 1;
break;
}
}
if (!found && client->configs.count < MAX_CONFIGS) {
strncpy(client->configs.entries[client->configs.count].key,
keyObj->valuestring, MAX_KEY_LEN - 1);
strncpy(client->configs.entries[client->configs.count].value,
valObj->valuestring, MAX_VAL_LEN - 1);
client->configs.count++;
if (client->onChange) {
printf("[New] %s = %s\n", keyObj->valuestring, valObj->valuestring);
client->onChange(keyObj->valuestring, valObj->valuestring);
}
}
}
}
}
/* 更新 lastTime */
cJSON *lastTimeObj = cJSON_GetObjectItem(dataObj, "lastTime");
if (lastTimeObj && cJSON_IsNumber(lastTimeObj)) {
client->lastTime = lastTimeObj->valueint;
}
cJSON_Delete(root);
return 0;
}
/* ============ 客户端 API ============ */
/**
* 初始化客户端
* @param client 客户端实例
* @param config 配置参数
*/
void config_client_init(ConfigClient *client, Config *config) {
client->config = *config;
client->configs.count = 0;
client->lastTime = 0;
client->onChange = NULL;
client->running = 0;
client->curl = curl_easy_init();
g_client = client;
/* 尝试从缓存加载 */
if (!load_from_cache(client)) {
printf("[Cache] No cache found, will fetch from server\n");
}
}
/**
* 拉取配置(全量或增量)
* @param client 客户端实例
* @param incremental 是否增量拉取
* @return 0 成功,-1 失败
*/
static int fetch_configs(ConfigClient *client, int incremental) {
char url[1024];
char payload[1024];
if (incremental && client->lastTime > 0) {
snprintf(url, sizeof(url), "%s/api/fetchChangedConfigs", client->config.baseUrl);
snprintf(payload, sizeof(payload),
"{\"appName\":\"%s\",\"env\":\"%s\",\"version\":\"%s\","
"\"ignoreGlobal\":%s,\"token\":\"%s\",\"lastTime\":%ld}",
client->config.appName, client->config.env, client->config.version,
client->config.ignoreGlobal ? "true" : "false",
client->config.token, client->lastTime);
} else {
snprintf(url, sizeof(url), "%s/api/fetchAllConfigs", client->config.baseUrl);
snprintf(payload, sizeof(payload),
"{\"appName\":\"%s\",\"env\":\"%s\",\"version\":\"%s\","
"\"ignoreGlobal\":%s,\"token\":\"%s\"}",
client->config.appName, client->config.env, client->config.version,
client->config.ignoreGlobal ? "true" : "false",
client->config.token);
}
printf("[%s] Fetching from %s...\n", incremental ? "Incremental" : "Full", url);
char *response = http_post(client, url, payload);
if (!response) {
fprintf(stderr, "Failed to fetch configs\n");
return -1;
}
int ret = parse_json_response(response, client);
free(response);
if (ret == 0) {
save_to_cache(client);
}
return ret;
}
/* 定时同步线程 */
static void *sync_thread_func(void *arg) {
ConfigClient *client = (ConfigClient *)arg;
while (client->running) {
sleep(client->config.syncInterval);
if (!client->running) break;
fetch_configs(client, 1);
}
return NULL;
}
/**
* 启动客户端
* @param client 客户端实例
*/
void config_client_start(ConfigClient *client) {
/* 首次全量拉取 */
fetch_configs(client, 0);
/* 启动定时同步线程 */
client->running = 1;
pthread_create(&client->syncThread, NULL, sync_thread_func, client);
printf("[Sync] Started background sync thread (interval: %ds)\n",
client->config.syncInterval);
}
/**
* 停止客户端
* @param client 客户端实例
*/
void config_client_stop(ConfigClient *client) {
client->running = 0;
if (client->syncThread) {
pthread_join(client->syncThread, NULL);
}
save_to_cache(client);
if (client->curl) {
curl_easy_cleanup(client->curl);
}
curl_global_cleanup();
printf("[Client] Stopped\n");
}