在嵌入式系统中,JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于设备与服务器之间的数据通信。cJSON 是一个用 C 语言实现的轻量级 JSON 解析和生成库,因其简单高效、资源占用低,非常适合嵌入式系统。下面我将详细介绍 cJSON 的功能、使用方法、适用场景,并通过经典示例帮助你理解其应用。
一、cJSON 是什么?为什么用它?
1. cJSON 简介
cJSON 是一个开源的 C 语言库,专门用于解析和生成 JSON 数据。它由 Dave Gamble 开发,代码简洁,易于移植,无需外部依赖,特别适合资源受限的嵌入式系统。cJSON 的主要功能包括:
解析 JSON:将 JSON 字符串解析为 C 语言中的数据结构。
生成 JSON:将 C 语言中的数据结构转换为 JSON 字符串。
操作 JSON:支持增删改查 JSON 对象、数组等。
2. 为什么在嵌入式系统中使用 cJSON?
轻量级:cJSON 代码量小,编译后占用空间少,适合内存受限的嵌入式设备(如 MCU)。
无依赖:纯 C 实现,易于移植到各种嵌入式平台(如 STM32、ESP32 等)。
高效:解析和生成 JSON 的性能较高,满足实时性要求。
简单易用:API 设计直观,开发者可以快速上手。
开源免费:MIT 许可证,适合商业和非商业项目。
3. 适用场景
cJSON 常用于以下嵌入式场景:
物联网(IoT)设备:设备与云端通过 JSON 格式交换数据,如传感器数据上传、远程配置下发。
通信协议:在 HTTP、MQTT、WebSocket 等协议中,JSON 作为数据载体,cJSON 用于处理这些数据。
配置文件解析:嵌入式设备使用 JSON 格式存储配置参数,cJSON 用于读取和修改。
调试和日志:将设备状态以 JSON 格式输出,便于调试和分析。
二、cJSON 的核心功能和数据结构
1. cJSON 的数据结构
cJSON 使用一个核心结构体 cJSON 来表示 JSON 数据,支持以下类型:
基本类型:cJSON_Number(数字)、cJSON_String(字符串)、cJSON_True/cJSON_False(布尔值)、cJSON_Null(空值)。
复合类型:cJSON_Object(对象)、cJSON_Array(数组)。
cJSON 结构体的定义(简化版)如下:
typedef struct cJSON {
struct cJSON *next, *prev; // 用于链表,数组或对象中的元素链接
struct cJSON *child; // 子节点(对象或数组的子元素)
int type; // 数据类型(如 cJSON_Number、cJSON_String 等)
char *valuestring; // 字符串值
int valueint; // 整数值
double valuedouble; // 浮点数值
char *string; // 键名(仅对象中使用)
} cJSON;
2. 核心 API
以下是 cJSON 常用的 API 函数,分为解析、生成和操作三大类:
解析 JSON:
cJSON_Parse(const char *value):将 JSON 字符串解析为 cJSON 结构。
cJSON_GetObjectItem(cJSON *object, const char *string):获取对象中指定键的值。
cJSON_GetArrayItem(cJSON *array, int index):获取数组中指定索引的元素。
生成 JSON:
cJSON_CreateObject():创建空 JSON 对象。
cJSON_CreateArray():创建空 JSON 数组。
cJSON_CreateString(const char *string):创建字符串节点。
cJSON_CreateNumber(double num):创建数字节点。
cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item):向对象添加键值对。
cJSON_Print(cJSON *item):将 cJSON 结构转换为 JSON 字符串。
操作 JSON:
cJSON_Delete(cJSON *item):释放 cJSON 结构内存。
cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem):替换对象中的键值。
cJSON_DetachItemFromArray(cJSON *array, int which):从数组中移除元素。
三、如何使用 cJSON?
1. 环境准备
获取 cJSON:从 GitHub(https://github.com/DaveGamble/cJSON)下载 cJSON.c 和 cJSON.h。
集成到项目:
将 cJSON.c 和 cJSON.h 加入嵌入式项目。
在编译器中确保包含标准 C 库(如 string.h、stdlib.h)。
对于资源极度受限的系统,可通过修改 cJSON.h 中的宏(如禁用某些功能)优化内存占用。
编译:将 cJSON 源码编译到你的嵌入式项目中,通常只需添加 cJSON.c 到项目文件列表。
2. 使用步骤
解析 JSON 字符串:
使用 cJSON_Parse 解析 JSON 字符串。
检查解析结果是否为空(防止无效 JSON)。
使用 cJSON_GetObjectItem 或 cJSON_GetArrayItem 提取数据。
生成 JSON 数据:
使用 cJSON_Create* 函数创建 JSON 节点。
使用 cJSON_AddItemTo* 函数构建对象或数组。
使用 cJSON_Print 转换为 JSON 字符串。
释放内存:
使用 cJSON_Delete 释放 cJSON 结构,避免内存泄漏。
四、经典示例
以下通过两个经典嵌入式场景示例,展示 cJSON 的使用方法。
示例 1:解析传感器数据的 JSON 字符串
假设设备接收到以下 JSON 数据,表示传感器状态:
{
"temperature": 25.5,
"humidity": 60,
"status": "OK"
}
代码实现:
#include
#include
#include "cJSON.h"
void parse_sensor_data(const char *json_str) {
// 解析 JSON 字符串
cJSON *root = cJSON_Parse(json_str goroutine
if (root == NULL) {
printf("Failed to parse JSON\n");
return;
}
// 提取数据
cJSON *temp = cJSON_GetObjectItem(root, "temperature");
cJSON *hum = cJSON_GetObjectItem(root, "humidity");
cJSON *status = cJSON_GetObjectItem(root, "status");
if (temp && hum && status) {
printf("Temperature: %.1f\n", temp->valuedouble);
printf("Humidity: %d\n", hum->valueint);
printf("Status: %s\n", status->valuestring);
} else {
printf("Missing fields in JSON\n");
}
// 释放内存
cJSON_Delete(root);
}
int main() {
const char *json_str = "{\"temperature\": 25.5, \"humidity\": 60, \"status\": \"OK\"}";
parse_sensor_data(json_str);
return 0;
}
解释:
cJSON_Parse 将 JSON 字符串解析为 cJSON 结构。
使用 cJSON_GetObjectItem 提取 temperature、humidity 和 status 的值。
检查每个字段是否存在,防止访问空指针。
最后用 cJSON_Delete 释放内存。
适用场景:嵌入式设备从云端接收 JSON 格式的传感器数据,解析后用于控制逻辑(如判断温度是否超标)。
输出:
Temperature: 25.5
Humidity: 60
Status: OK
示例 2:生成设备状态的 JSON 字符串
假设设备需要生成以下 JSON 数据发送到服务器:
{
"device_id": "DEV001",
"battery": 85,
"sensors": [
{"type": "temp", "value": 26.2},
{"type": "hum", "value": 58}
]
}
代码实现:
#include
#include
#include "cJSON.h"
void create_device_status() {
// 创建 JSON 对象
cJSON *root = cJSON_CreateObject();
// 添加基本字段
cJSON_AddStringToObject(root, "device_id", "DEV001");
cJSON_AddNumberToObject(root, "battery", 85);
// 创建传感器数组
cJSON *sensors = cJSON_CreateArray();
cJSON_AddItemToObject(root, "sensors", sensors);
// 添加第一个传感器数据
cJSON *sensor1 = cJSON_CreateObject();
cJSON_AddStringToObject(sensor1, "type", "temp");
cJSON_AddNumberToObject(sensor1, "value", 26.2);
cJSON_AddItemToArray(sensors, sensor1);
// 添加第二个传感器数据
cJSON *sensor2 = cJSON_CreateObject();
cJSON_AddStringToObject(sensor2, "type", "hum");
cJSON_AddNumberToObject(sensor2, "value", 58);
cJSON_AddItemToArray(sensors, sensor2);
// 转换为 JSON 字符串
char *json_str = cJSON_Print(root);
printf("%s\n", json_str);
// 释放内存
cJSON_Delete(root);
free(json_str);
}
int main() {
create_device_status();
return 0;
}
解释:
使用 cJSON_CreateObject 和 cJSON_CreateArray 创建 JSON 结构。
通过 cJSON_Add* 函数添加键值对和数组元素。
cJSON_Print 生成 JSON 字符串,供设备通过网络发送。
释放 root 和 json_str 的内存,防止泄漏。
适用场景:嵌入式设备(如 ESP32)生成状态数据,通过 MQTT 或 HTTP 发送到云端。
输出:
{
"device_id": "DEV001",
"battery": 85,
"sensors": [{
"type": "temp",
"value": 26.2
}, {
"type": "hum",
"value": 58
}]
}
五、在嵌入式系统中的使用注意事项
内存管理:
嵌入式系统内存有限,始终使用 cJSON_Delete 释放 cJSON 结构。
使用 cJSON_PrintUnformatted 生成无格式 JSON 字符串,节省空间。
对于极低内存设备,可通过 #define cJSON_Malloc 自定义内存分配函数,优化内存使用。
错误处理:
检查 cJSON_Parse 返回值是否为 NULL。
使用 cJSON_Is* 函数(如 cJSON_IsNumber、cJSON_IsString)验证字段类型。
性能优化:
避免频繁解析和生成大型 JSON 数据。
对于固定格式的 JSON,可使用静态结构减少动态分配。
移植性:
cJSON 默认使用标准 C 库的 malloc 和 free,确保嵌入式平台的 C 库支持这些函数。
若平台不支持浮点数运算,可禁用 cJSON_Number 的浮点支持(修改 cJSON.h)。
六、适用场景和经典案例
物联网设备与云端通信:
场景:ESP32 采集温湿度数据,通过 MQTT 协议以 JSON 格式发送到 AWS IoT。
实现:使用示例 2 生成 JSON 数据,调用 MQTT 库发送。
优点:JSON 格式通用,易于云端解析;cJSON 轻量,适合 ESP32 的 520KB SRAM。
设备配置管理:
场景:STM32 设备从 Flash 读取 JSON 格式的配置文件,调整运行参数。
实现:使用示例 1 解析 JSON,提取参数值。
优点:JSON 格式直观,易于手动编辑配置文件。
调试日志输出:
场景:设备通过串口输出 JSON 格式的运行状态,便于调试工具解析。
实现:类似示例 2,生成 JSON 后通过 UART 输出。
优点:结构化日志便于自动化分析。
七、总结
cJSON 是一个轻量、简单、高效的 JSON 解析和生成库,非常适合嵌入式系统。其核心优势在于无依赖、易移植和低资源占用。通过解析和生成 JSON,cJSON 能满足物联网设备通信、配置管理等需求。使用时需注意内存管理和错误处理,尤其在资源受限的嵌入式环境中。