61、JSON解析:cJSON库集成、JSON构造与解析、网络数据传输
JSON,说白了就是网络世界里的“普通话”。
你想想看,C语言程序要跟Web服务器通信,或者跟Python、Java写的后端交换数据,总得有个双方都认的格式吧?JSON就是干这个的。它轻量、可读性强,而且几乎所有语言都有现成的解析库。
我个人习惯在嵌入式网络项目里用cJSON,这个库就一个.c和一个.h文件,集成起来非常方便。今天我们就把它玩透。
61.1 为什么是cJSON?
我刚开始做网络编程时,也想过自己手写JSON解析器。后来发现,这玩意儿看着简单,坑却不少——字符串转义、嵌套对象、数组边界……随便一个没处理好,程序就崩了。
cJSON是纯C写的,不依赖任何外部库。它用起来就像操作一棵树——每个节点要么是对象、要么是数组、要么是值。你想想看,这不就是数据结构课上学过的树形结构吗?
核心特点:
- 单文件集成,复制粘贴就能用
- 支持嵌套对象和数组
- 内存管理透明,自己分配自己释放
- 解析速度快,适合嵌入式场景
61.2 集成cJSON到项目
集成方式简单到离谱。你只需要从GitHub上下载cJSON.h和cJSON.c,然后扔到你的项目目录里,直接编译就行。
// 在你的代码中包含头文件
#include "cJSON.h"
// 编译时记得加上cJSON.c
// gcc -o myapp main.c cJSON.c -lm
嗯,这里要注意:cJSON用到了数学库,所以链接时别忘了加 -lm。我曾经因为这个坑排查了半天,最后发现是链接参数漏了。
61.3 构造JSON数据
构造JSON,说白了就是创建一棵树。先创建根节点,然后往里面挂子节点。
// 创建一个根对象
cJSON *root = cJSON_CreateObject();
// 添加字符串字段
cJSON_AddStringToObject(root, "device_id", "sensor_01");
// 添加数字字段
cJSON_AddNumberToObject(root, "temperature", 25.6);
// 添加布尔字段
cJSON_AddBoolToObject(root, "online", 1);
// 添加数组
cJSON *data_array = cJSON_CreateArray();
cJSON_AddItemToObject(root, "readings", data_array);
// 往数组里加元素
cJSON_AddItemToArray(data_array, cJSON_CreateNumber(23.4));
cJSON_AddItemToArray(data_array, cJSON_CreateNumber(24.1));
cJSON_AddItemToArray(data_array, cJSON_CreateNumber(25.0));
// 转成字符串
char *json_str = cJSON_Print(root);
printf("JSON: %s\n", json_str);
// 用完释放
cJSON_Delete(root);
free(json_str);
你看,整个过程就像搭积木。我习惯先画好JSON的结构图,再动手写代码,这样不容易漏字段。
61.4 解析JSON数据
解析是构造的逆过程。从网络收到一串JSON字符串,把它解析成cJSON树,然后遍历取值。
// 假设从网络收到了这个字符串
char *recv_json = "{ \"status\": \"ok\", \"value\": 42 }";
// 解析
cJSON *parsed = cJSON_Parse(recv_json);
if (parsed == NULL) {
printf("解析失败: %s\n", cJSON_GetErrorPtr());
return -1;
}
// 取值
cJSON *status = cJSON_GetObjectItem(parsed, "status");
if (cJSON_IsString(status) && (status->valuestring != NULL)) {
printf("status = %s\n", status->valuestring);
}
cJSON *value = cJSON_GetObjectItem(parsed, "value");
if (cJSON_IsNumber(value)) {
printf("value = %d\n", value->valueint);
}
// 释放
cJSON_Delete(parsed);
避坑指南:
我曾经在解析嵌套JSON时,忘记检查子节点是否为NULL,结果程序直接段错误。记住:每次调用cJSON_GetObjectItem后,都要判断返回值。网络数据不可信,你永远不知道对方会发什么过来。
61.5 网络数据传输中的JSON
实际项目中,JSON通常作为HTTP请求体或WebSocket消息的载体。我举个TCP传输的例子:
// 发送端:构造JSON并发送
char *json_data = cJSON_Print(root);
send(sockfd, json_data, strlen(json_data), 0);
// 接收端:接收并解析
char buffer[4096] = {0};
int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
cJSON *recv_root = cJSON_Parse(buffer);
// 处理数据...
cJSON_Delete(recv_root);
}
这里有个细节:网络传输是流式的,一次recv可能只收到部分数据。我建议先收完完整的一帧再解析。常用的做法是:发送时先发4字节长度头,再发JSON数据;接收时先读长度,再读指定字节数。
61.6 知识体系总览
下面这张图,是我梳理的JSON处理核心流程。你看一遍,心里就有谱了。
61.7 实战经验总结
做了这么多年网络编程,我总结了几条关于JSON的心得:
| 场景 | 推荐做法 | 不推荐 |
|---|---|---|
| 构造JSON | 用cJSON_AddXXX系列函数 | 手动拼接字符串 |
| 解析JSON | 先Parse,再GetObjectItem | 用sscanf硬解析 |
| 内存管理 | 配对使用Create/Delete | 只Create不Delete |
| 网络传输 | 加长度头,分帧传输 | 直接发JSON字符串 |
个人小技巧:
我习惯在调试阶段用 cJSON_Print 打印格式化JSON,方便肉眼检查。上线前再换成 cJSON_PrintUnformatted,减少网络传输量。这个细节,能省不少带宽。
JSON解析这事儿,说白了就是“构造-传输-解析”三步走。cJSON帮我们封装好了底层细节,你只要记住:创建了就要释放,取到了就要检查。做到这两点,基本不会出大问题。
好了,这一章就到这里。代码写完了,记得跑一遍测试,看看内存有没有泄漏。