From 7bf1dff3cef5415af027db6eac2fb17830c25c4a Mon Sep 17 00:00:00 2001 From: Odboy Date: Wed, 15 Apr 2026 21:02:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(c-client):=20=E5=AE=9E=E7=8E=B0=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E7=9A=84=20C=20=E5=AE=A2=E6=88=B7=E7=AB=AF=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能: - 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 --- kenaito-conf-c-client-demo/main.c | 117 +++++++- kenaito-conf-c-client/README.md | 66 ++++- kenaito-conf-c-client/main.c | 426 +++++++++++++++++++++++++----- 3 files changed, 531 insertions(+), 78 deletions(-) diff --git a/kenaito-conf-c-client-demo/main.c b/kenaito-conf-c-client-demo/main.c index e707ff6..1be23bd 100644 --- a/kenaito-conf-c-client-demo/main.c +++ b/kenaito-conf-c-client-demo/main.c @@ -1,8 +1,119 @@ +/** + * Kenaito Config Center C Client Demo + * + * 演示如何使用 C 客户端 SDK: + * - 初始化客户端 + * - 注册配置变更回调 + * - 启动/停止客户端 + * - 读取配置 + * + * 编译: + * gcc -o demo main.c -I../kenaito-conf-c-client -L../kenaito-conf-c-client -lkenaito_conf -lcurl -lcjson -lpthread + * + * 或直接编译所有源文件: + * gcc -o demo main.c ../kenaito-conf-c-client/main.c -lcurl -lcjson -lpthread + * + * 运行: + * ./demo + */ + #include +#include +#include +#include +#include "../kenaito-conf-c-client/main.c" + +/* 全局客户端实例,用于信号处理 */ +static ConfigClient *g_demo_client = NULL; + +/** + * 配置变更回调示例 + */ +void on_config_change(const char *key, const char *value) { + printf("\n[Callback] Config changed: %s = %s\n", key, value); + + /* 示例:根据配置键执行不同操作 */ + if (strcmp(key, "log.level") == 0) { + printf(" -> Log level changed to: %s\n", value); + } else if (strcmp(key, "database.host") == 0) { + printf(" -> Database host changed to: %s\n", value); + /* 实际应用中可在此重新初始化数据库连接 */ + } else if (strcmp(key, "database.port") == 0) { + printf(" -> Database port changed to: %s\n", value); + } else if (strcmp(key, "feature.enabled") == 0) { + printf(" -> Feature flag changed: %s\n", value); + } +} + +/** + * 信号处理 - 优雅退出 + */ +static void signal_handler(int sig) { + printf("\n[Signal] Received signal %d, shutting down...\n", sig); + if (g_demo_client) { + g_demo_client->running = 0; + } +} int main() { - printf("Kenaito Config Center C Client Demo\n"); - printf("AppName: kenaito-springboot-demo, Env: daily\n"); - printf("See ../kenaito-conf-c-client/main.c for full implementation\n"); + printf("========================================\n"); + printf(" Kenaito Config Center C Client Demo\n"); + printf("========================================\n\n"); + + /* 注册信号处理 */ + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + /* 初始化 CURL */ + curl_global_init(CURL_GLOBAL_DEFAULT); + + /* 配置客户端 */ + Config config = { + .baseUrl = "http://localhost:8080", + .appName = "kenaito-springboot-demo", + .env = "daily", + .version = "latest", + .token = "", + .syncInterval = 10, + .ignoreGlobal = 0, + .cacheDir = "." + }; + + ConfigClient client; + config_client_init(&client, &config); + client.onChange = on_config_change; + + g_demo_client = &client; + + /* 启动客户端 */ + config_client_start(&client); + + /* 主循环:演示配置读取 */ + printf("\n[Demo] Running... Press Ctrl+C to stop\n\n"); + + int iterations = 0; + while (client.running && iterations < 10) { + sleep(5); + + /* 演示:定期读取并打印配置 */ + const char *dbHost = config_client_get(&client, "database.host"); + const char *dbPort = config_client_get(&client, "database.port"); + const char *logLevel = config_client_get(&client, "log.level"); + const char *featureEnabled = config_client_get(&client, "feature.enabled"); + + printf("\n[Current Configs @ %ld]\n", time(NULL)); + printf(" database.host = %s\n", dbHost ? dbHost : "(not set)"); + printf(" database.port = %s\n", dbPort ? dbPort : "(not set)"); + printf(" log.level = %s\n", logLevel ? logLevel : "(not set)"); + printf(" feature.enabled = %s\n", featureEnabled ? featureEnabled : "(not set)"); + printf(" Total configs: %d\n\n", client.configs.count); + + iterations++; + } + + /* 停止客户端 */ + config_client_stop(&client); + + printf("\n[Demo] Finished\n"); return 0; } diff --git a/kenaito-conf-c-client/README.md b/kenaito-conf-c-client/README.md index 1c9c5eb..491e85c 100644 --- a/kenaito-conf-c-client/README.md +++ b/kenaito-conf-c-client/README.md @@ -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 -#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; diff --git a/kenaito-conf-c-client/main.c b/kenaito-conf-c-client/main.c index 66f36ce..9147152 100644 --- a/kenaito-conf-c-client/main.c +++ b/kenaito-conf-c-client/main.c @@ -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 #include #include #include +#include +#include +#include #include #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"); +}