cJSON库应用
目录
一、前言
二、JSON
2.1 JSON介绍
2.2 JSON规范详解
2.2.1 四种常规的值类型
2.2.2 两种允许内嵌其他值的类型
2.3 JSON的应用场景
三、cJSON
3.1 数据结构
3.2 构造JSON数据
3.3 打印JSON数据
3.4 解析JSON数据
3.5 内存问题
3.6 示例
四、结语
一、前言
后续会更新ESP32的HTTP客户端应用,使用HTTP协议和服务器进行通信,数据格式为JSON,因此需要用到cJSON库来构造和解析JSON数据。
二、JSON
JSON是一种优秀的文本数据格式。
2.1 JSON介绍
JSON是一种文本数据格式,来源于编程语言Javascript的对象语法,但是JSON是独立于Javascript的,两者并无强关联。简单来说,JSON就是一种文本规范,或者说是一种字符串规范,如下都是符合JSON规范的文本:
JSON的优点是可以简单灵活的表示树形结构的数据,如下:
通过简单的一条字符串,就可以记录多层结构的数据。
2.2 JSON规范详解
JSON支持6种类型的值,分别是字符串、数字、布尔值、null、对象、数组。
2.2.1 四种常规的值类型
● 字符串
字符串是包含在双引号内的多个字符,如果字符串内包含双引号,则需要使用 \ 转义符
● 数字
数字可以是整数、浮点数
● 布尔值
true、false
● null
空值
上面这四种常规类型的值是不允许内嵌其他值的,所以JSON文本可以仅含有一个字符或一个数字。
2.2.2 两种允许内嵌其他值的类型
● 对象
对象是键值对的字典,如果刷过哈希表的应该很熟悉,像我之前的项目用到了MySQL数据库,也涉及到了键值对的概念。简单来说就是通过键名来找到值。
对象需要包含在一对 {} 内,一个对象内部的多个键值对需要用逗号隔开。键值对的“键”必须用字符串,而值可以是JSON支持的六种值的任意一种,且一个对象内的多个值无需统一类型。
● 数组
数组需要包含在一对 [] 内,一个数组内的多个值用逗号隔开,数组内的值可以是JSON支持的六种值的任意一种,且一个数组内的多个值无需统一类型。
得益于对象、数组这两种允许内嵌的值类型,JSON文本能表示复杂的树形数据,一般都会采用对象或数组作为最外层结构。
2.3 JSON的应用场景
JSON这种文本规范一般适合数据量不大、需要记录树形数据或需要灵活扩展数据时使用。
三、cJSON
cJSON是一个用C语言编写的轻量级JSON处理库,它的核心代码只有 cJSON.h 和 cJSON.c 两个文件。
从 https://github.com/DaveGamble/cJSON 取得最新代码即可。
3.1 数据结构
cJSON的基本数据结构是 cJSON 结构体,因为JSON数据一般都使用对象作为最外层结构,即使只有一个数据,也是一个键值对;如果有多个键值对,用链表来进行管理比较方便,因为链表具有灵活扩展数据的特性,那么每个cJSON结构体都是链表的一个节点:
/* The cJSON structure: */
typedef struct cJSON
{/* next/prev 允许你遍历 数组/对象 链表。或者使用GetArraySize/GetArrayItem/GetObjectItem */struct cJSON *next; //指向下一个节点(键值对)struct cJSON *prev; //指向上一个节点(键值对)/* 一个数组或对象将有一个子指针,指向该数组/对象内部的链表。因为JSON数据支持嵌套,一个键值对的值会是一个对象/数据(都用链表表示)*/struct cJSON *child;/* 表示该键值对中值的类型 */int type;/* 如果键值对中值的类型(type)是字符串,该指针指向键值 */char *valuestring;/* 如果键值对中值的类型(type)是整数,该变量存储整数值向 valueint 写入已被弃用,请改用 cJSON_SetNumberValue */int valueint;/* 如果键值对中值的类型(type)是浮点数,该变量存储浮点数值 */double valuedouble;/* 该键值对的名称 */char *string;
} cJSON;
其中,type定义如下:
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
● cJSON_Invalid 无效值
● cJSON_False 布尔值false
● cJSON_True 布尔值true
● cJSON_NULL 空值null
● cJSON_Number 数值,对应的数值同时存放在 value_int 和 value_double 中
● cJSON_String 字符串值,表示以\0结尾的字符串,存放在 value_string 中
● cJSON_Array 数组,包含一个或多个cJSON对象,它们通过 child 指向的链表表示,并通过 prev ,next 连接在一起。数组第一项 prev==NULL ,最后一项 next==NULL ,所以它不是一个双向循环链表
● cJSON_Object 对象,内部存储方式类似 cJSON_Array ,但对象的各个键值对的键名存储在各自的 string 中
● cJSON_Raw raw值,不用于解析,具体见官方文档
3.2 构造JSON数据
cJSON作为JSON格式的解析库,要做的无非就是构造和解析JSON格式的数据,我们先来看看如何构造一个JSON数据。
构造JSON数据,其实就是向链表尾部插入节点、或者插入一条新的链表。前面说过:JSON数据一般都使用对象作为最外层结构,即一个JSON文本由花括号{}包围。那么这个最外面的{},就是JSON数据的根节点,后续不断往这个{}里面添加键值对(添加节点),最终就能构造出JSON格式的数据。
① 创建根节点
cJSON *root = cJSON_CreateObject();
② 往对象里添加键值对(往链表里添加节点)
/* 辅助函数,创建项目的同时向对象添加项目 */
// 向对象添加一个null值,需传入键名
cJSON *cJSON_AddNullToObject(cJSON * const object, const char * const name);
// 向对象添加一个true布尔值,需传入键名
cJSON *cJSON_AddTrueToObject(cJSON * const object, const char * const name);
// 向对象添加一个false布尔值,需传入键名
cJSON *cJSON_AddFalseToObject(cJSON * const object, const char * const name);
// 向对象添加一个布尔值,需传入键名和布尔值
cJSON *cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
// 向对象添加一个数值,需传入键名和数值
cJSON *cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
// 向对象添加一个字符串值,需传入键名和字符串
cJSON *cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
// 向对象添加一个raw值
cJSON *cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
// 嵌套一个对象
cJSON *cJSON_AddObjectToObject(cJSON * const object, const char * const name);
// 嵌套一个数组
cJSON *cJSON_AddArrayToObject(cJSON * const object, const char * const name);
上面这些都是比较便捷的函数。它们成功时返回被添加的项目、失败返回NULL,因此有时需要获得它们的返回值,比如嵌套对象和数组,需要在嵌套的对象或数组里再添加项目(节点),就需要得到对象的头节点。
你也可以用比较朴素的方法,调用下面这些函数:
// 创建一个数组,获得它的指针
cJSON *cJSON_CreateArray(void);
// 向对象里添加一个项目(嵌套对象/数组)
cJSON_bool cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
// 向数组里添加一个项目(嵌套对象/数组)
cJSON_bool cJSON_AddItemToArray(cJSON *array, cJSON *item);
“项目”指的就是“节点”,一个节点就是一个cJSON结构体。理解了这个就能构造任何想要的JSON格式的数据。
3.3 打印JSON数据
使用以下函数即可,传入根节点,因为构造好的JSON数据就是一条长长的链表:
/* Render a cJSON entity to text for transfer/storage. */
char *cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
char *cJSON_PrintUnformatted(const cJSON *item);
返回的字符串直接打印出来就行。
3.4 解析JSON数据
得到一个JSON格式的字符串后,调用下面这个函数就可对其进行解析,并返回根节点的指针:
cJSON *cJSON_Parse(const char *value);
然后就像查字典一样,通过 key(键名)获得键值对的指针,第一个参数传入根节点的指针:
cJSON *cJSON_GetObjectItem(const cJSON * const object, const char * const string);
如果JSON数据的值是地址,使用下面两个API提取数据:
int cJSON_GetArraySize(const cJSON *array);
cJSON *cJSON_GetArrayItem(const cJSON *array, int index);
3.5 内存问题
由于本质上是对链表的操作,每创建一个节点都会使用malloc函数从堆中分配内存,如果使用完了不释放,会导致内存泄漏,调用下面这个函数释放内存:
void cJSON_Delete(cJSON *item);
传入根节点,函数内部会遍历链表,将所有嵌套的链表一并删除。所以这个函数也可以用于删除某一条嵌套的链表。
3.6 示例
我们举一些实际的例子来看看如何使用这些函数,并且给出一些模板。
假设我们需要上报一款医疗设备的使用记录,JSON数据格式如下:
{"sn" : "Sakabu","usageLogs" : [{"sequence" : 0,"channel" : "A","plan" : "P01","duration" : 20},{"sequence" : 1,"channel" : "B","plan" : "P03","duration" : 50}]
}
需要上传序列号和使用记录。使用记录是一个值为数组的键值对。数组里的每个元素都是一个对象,里面包含了使用序号、通道、方案和持续时间。现在我们来看看如何构造这个JSON数据,并把它打印出来。
// 构建设备
root = cJSON_CreateObject();
// 设置设备序列号
cJSON_AddStringToObject(root, "sn", "Sakabu");
// 构建JSON数组
cJSON *logs_array = cJSON_CreateArray();
// 第一条使用记录
cJSON *log_item = cJSON_CreateObject();
cJSON_AddNumberToObject(log_item, "sequence", 0);
cJSON_AddStringToObject(log_item, "channel", "A");
cJSON_AddStringToObject(log_item, "plan", "P01");
cJSON_AddNumberToObject(log_item, "duration", 20);
cJSON_AddItemToArray(logs_array, log_item);// 第二条使用记录
cJSON *log_item = cJSON_CreateObject();
cJSON_AddNumberToObject(log_item, "sequence", 1);
cJSON_AddStringToObject(log_item, "channel", "B");
cJSON_AddStringToObject(log_item, "plan", "P03");
cJSON_AddNumberToObject(log_item, "duration", 50);
cJSON_AddItemToArray(logs_array, log_item);
cJSON_AddItemToObject(root, "usageLogs", logs_array);// 转换为字符串
char *json_str = cJSON_PrintUnformatted(root);
if (!json_str)
{printf("JSON序列化失败");return 0;
}
// 打印JSON数据
printf("上报的JSON数据: %s", json_str);
// 释放JSON对象
cJSON_Delete(root);
// 释放内存
free(json_str);
现在又假设我们上报完数据后,需要接收服务器响应的JSON数据,并解析数据,收到的JSON数据如下:
{"code": 200,"msg": "OK","data": null
}
解析函数如下:
/*** 通用的JSON响应解析函数 - 检查响应是否成功* @param json_str 要解析的数据* @param data 出参,指向cJSON对象的指针* @return esp_err_t 执行结果*/
bool parse_response_json(const char *json_str, cJSON **data)
{cJSON *root = cJSON_Parse(json_str);if (root == NULL){printf("JSON解析失败: %s", cJSON_GetErrorPtr());return false;}// 检查code字段是否为200(表示成功)cJSON *code = cJSON_GetObjectItem(root, "code");if (!code || !cJSON_IsNumber(code) || code->valueint != 200){// 尝试获取错误信息cJSON *msg = cJSON_GetObjectItem(root, "msg");if (msg && cJSON_IsString(msg)){printf("API请求失败: %s", msg->valuestring);}else{printf("API请求失败: 响应码非200");}cJSON_Delete(root);return false;}// 更新data指针,解引用将数据拷贝*data = cJSON_GetObjectItem(root, "data");return true;
}
上面这个解析函数适用于和服务器使用HTTP协议进行通信,获得服务器的返回数据后解析(一般成功会响应200)。
使用方式如下:
// 解析响应数据
cJSON *data = NULL;
cJSON *root_json = NULL;
// 注意:这里需要传入指针变量的地址
result = parse_response_json(response_buffer, &data);
if (result != true || data == NULL)
{printf("解析设备配置响应失败");return false;
}
// 获取root对象用于后续释放
root_json = cJSON_Parse(response_buffer);/* 其他操作 */cJSON_Delete(root_json);
值得注意的是,解析函数内部获取 data 节点的函数 cJSON_GetObjectItem(root, "data"); 返回的是cJSON指针类型,因此解析函数的第二个参数需要传入一个二级指针。如果传入的是一个一级指针(cJSON *data),函数内部会获得指针的副本,然后修改副本的值(如data = new_address;),但这样不会影响调用方的原始指针,因而获取不到想要的结果。
最后,记得释放内存。
四、结语
有需要补充的欢迎评论区留言,下期更新ESP32 HTTP客户端。