21、JSON解析:cJSON库集成、解析与构建、配置文件JSON化
说到JSON,我估计大家都不陌生。前后端交互用它,配置文件用它,就连一些嵌入式设备也在用。但说实话,在C语言里处理JSON,早期真挺痛苦的——自己写解析器?太费劲。用别人的库?又怕太重量级。
后来我遇到了cJSON,嗯,就是那个只有两个文件、轻量到不行的JSON解析库。今天咱们就聊聊怎么把它集成到项目里,怎么解析和构建JSON,以及怎么把配置文件做成JSON格式。
为什么选cJSON?
市面上C语言的JSON库不少,比如Jansson、json-c、parson等等。我个人习惯用cJSON,原因有三:
- 够轻量——就两个文件,cJSON.c和cJSON.h,不依赖别的库
- API简洁——核心函数不超过20个,上手快
- MIT协议——商用随便用,没坑
我在项目中遇到过好几次,团队里有人想用json-c,结果编译时发现要链接一堆依赖库,最后又换回cJSON。说白了,对于命令行工具这种场景,轻量才是王道。
集成cJSON到项目
集成方式很简单,就三步:
- 把cJSON.c和cJSON.h拷到你的项目目录
- 在代码里#include "cJSON.h"
- 编译时把cJSON.c一起编译
举个例子,你的项目结构可以这样:
my_tool/
├── src/
│ ├── main.c
│ ├── config.c
│ └── cJSON.c
├── include/
│ ├── config.h
│ └── cJSON.h
└── Makefile
编译命令也很直接:
gcc -Iinclude src/main.c src/config.c src/cJSON.c -o my_tool
解析JSON:从字符串到结构体
解析JSON,说白了就是把一段JSON字符串变成cJSON对象树,然后从树里取数据。
来看一个实际例子。假设我们有这样一个配置文件:
{
"server": {
"host": "127.0.0.1",
"port": 8080,
"workers": 4
},
"logging": {
"level": "info",
"file": "/var/log/app.log"
}
}
解析它的代码长这样:
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
int parse_config(const char *json_str) {
cJSON *root = cJSON_Parse(json_str);
if (root == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
fprintf(stderr, "解析错误: %s\n", error_ptr);
}
return -1;
}
// 取server对象
cJSON *server = cJSON_GetObjectItem(root, "server");
if (cJSON_IsObject(server)) {
cJSON *host = cJSON_GetObjectItem(server, "host");
cJSON *port = cJSON_GetObjectItem(server, "port");
cJSON *workers = cJSON_GetObjectItem(server, "workers");
if (cJSON_IsString(host) && cJSON_IsNumber(port) && cJSON_IsNumber(workers)) {
printf("Host: %s\n", host->valuestring);
printf("Port: %d\n", port->valueint);
printf("Workers: %d\n", workers->valueint);
}
}
// 取logging对象
cJSON *logging = cJSON_GetObjectItem(root, "logging");
if (cJSON_IsObject(logging)) {
cJSON *level = cJSON_GetObjectItem(logging, "level");
cJSON *file = cJSON_GetObjectItem(logging, "file");
if (cJSON_IsString(level) && cJSON_IsString(file)) {
printf("Log Level: %s\n", level->valuestring);
printf("Log File: %s\n", file->valuestring);
}
}
cJSON_Delete(root);
return 0;
}
构建JSON:从结构体到字符串
构建JSON比解析更直观。你想想看,就是创建对象、添加键值对、最后转成字符串。
下面这个例子,构建一个网络配置的JSON:
char *build_network_config(void) {
cJSON *root = cJSON_CreateObject();
if (root == NULL) return NULL;
// 添加字符串
cJSON_AddStringToObject(root, "interface", "eth0");
// 添加数字
cJSON_AddNumberToObject(root, "mtu", 1500);
// 添加布尔值
cJSON_AddBoolToObject(root, "dhcp", 1);
// 添加数组
cJSON *dns = cJSON_CreateArray();
cJSON_AddItemToArray(dns, cJSON_CreateString("8.8.8.8"));
cJSON_AddItemToArray(dns, cJSON_CreateString("8.8.4.4"));
cJSON_AddItemToObject(root, "dns_servers", dns);
// 添加嵌套对象
cJSON *ipv4 = cJSON_CreateObject();
cJSON_AddStringToObject(ipv4, "address", "192.168.1.100");
cJSON_AddStringToObject(ipv4, "netmask", "255.255.255.0");
cJSON_AddItemToObject(root, "ipv4", ipv4);
// 转成字符串
char *json_str = cJSON_Print(root);
if (json_str == NULL) {
fprintf(stderr, "JSON序列化失败\n");
cJSON_Delete(root);
return NULL;
}
cJSON_Delete(root);
return json_str;
}
生成的JSON长这样:
{
"interface": "eth0",
"mtu": 1500,
"dhcp": true,
"dns_servers": ["8.8.8.8", "8.8.4.4"],
"ipv4": {
"address": "192.168.1.100",
"netmask": "255.255.255.0"
}
}
配置文件JSON化:实战经验
把配置文件做成JSON格式,好处很明显:可读性强、层次清晰、扩展方便。但也有一些坑要注意。
我曾经在做一个日志分析工具时,把配置写成了JSON。一开始觉得挺爽,后来发现用户经常把配置写错——多一个逗号、少一个引号,解析就挂了。嗯,这里要注意,给用户用的工具,一定要做好错误提示。
下面是我总结的配置文件JSON化最佳实践:
| 实践要点 | 说明 |
|---|---|
| 提供默认值 | 解析时如果某个字段缺失,用默认值代替,不要直接报错 |
| 校验字段类型 | 用cJSON_IsString、cJSON_IsNumber等函数检查类型 |
| 友好的错误提示 | 告诉用户哪一行、哪个字段出错了 |
| 支持注释 | cJSON原生不支持注释,但可以在解析前用预处理去掉//和/* */ |
| 版本兼容 | 在配置里加一个"version"字段,方便以后升级 |
来看一个带默认值和校验的完整示例:
typedef struct {
char host[256];
int port;
int workers;
char log_level[16];
char log_file[512];
} AppConfig;
int load_config(const char *filepath, AppConfig *config) {
// 设置默认值
snprintf(config->host, sizeof(config->host), "localhost");
config->port = 8080;
config->workers = 4;
snprintf(config->log_level, sizeof(config->log_level), "info");
snprintf(config->log_file, sizeof(config->log_file), "/var/log/app.log");
// 读取文件
FILE *fp = fopen(filepath, "r");
if (fp == NULL) {
fprintf(stderr, "无法打开配置文件: %s\n", filepath);
return -1;
}
fseek(fp, 0, SEEK_END);
long fsize = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *json_str = malloc(fsize + 1);
fread(json_str, 1, fsize, fp);
json_str[fsize] = '\0';
fclose(fp);
// 解析JSON
cJSON *root = cJSON_Parse(json_str);
free(json_str);
if (root == NULL) {
const char *err = cJSON_GetErrorPtr();
fprintf(stderr, "JSON解析失败: %s\n", err ? err : "未知错误");
return -1;
}
// 读取server配置,缺失则用默认值
cJSON *server = cJSON_GetObjectItem(root, "server");
if (cJSON_IsObject(server)) {
cJSON *host = cJSON_GetObjectItem(server, "host");
if (cJSON_IsString(host)) {
snprintf(config->host, sizeof(config->host), "%s", host->valuestring);
}
cJSON *port = cJSON_GetObjectItem(server, "port");
if (cJSON_IsNumber(port)) {
config->port = port->valueint;
}
cJSON *workers = cJSON_GetObjectItem(server, "workers");
if (cJSON_IsNumber(workers)) {
config->workers = workers->valueint;
}
}
// 读取logging配置
cJSON *logging = cJSON_GetObjectItem(root, "logging");
if (cJSON_IsObject(logging)) {
cJSON *level = cJSON_GetObjectItem(logging, "level");
if (cJSON_IsString(level)) {
snprintf(config->log_level, sizeof(config->log_level), "%s", level->valuestring);
}
cJSON *file = cJSON_GetObjectItem(logging, "file");
if (cJSON_IsString(file)) {
snprintf(config->log_file, sizeof(config->log_file), "%s", file->valuestring);
}
}
cJSON_Delete(root);
return 0;
}
核心知识体系
下面这张图,把cJSON的核心操作和配置文件JSON化的流程串起来了:
这张图把整个流程分成了五层:从输入JSON字符串开始,经过解析、核心操作、输出,最后一定要释放内存。每一步都有对应的cJSON函数,你照着这个流程写代码,基本不会出错。
避坑指南
最后分享几个我踩过的坑:
- 内存泄漏——每次cJSON_Parse和cJSON_Print后,记得配对cJSON_Delete和free。我曾经在循环里解析JSON,忘了释放,跑了一晚上内存涨到2GB。
- 空指针检查——cJSON_GetObjectItem可能返回NULL,一定要判断。别问我怎么知道的,线上崩过一次就知道了。
- 字符串拷贝——cJSON对象释放后,它的valuestring指针就失效了。如果你要长期保存字符串,用snprintf拷出来。
- 大文件处理——如果配置文件超过10MB,建议用流式解析,不要一次性读到内存里。cJSON虽然方便,但不适合超大JSON。
好了,关于cJSON的集成、解析与构建,以及配置文件JSON化,就聊这么多。这些技巧我在好几个项目里都用过,效果不错。你写命令行工具时,不妨试试把配置做成JSON格式,维护起来会轻松很多。
公众号:蓝海资料掘金营,微信deep3321