当前位置: 首页 > news >正文

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.hcJSON.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_intvalue_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客户端。

http://www.lryc.cn/news/611282.html

相关文章:

  • C语言的常见错误与调试
  • uniapp renderjs 逻辑层,视图层互相传递数据封装
  • 背包初步练习
  • 计算机视觉面试保温:CLIP(对比语言-图像预训练)和BERT技术概述
  • Linux逻辑卷管理操作指南
  • 论文解读:Mamba: Linear-Time Sequence Modeling with Selective State Spaces
  • JSP相关Bug解决
  • AutoSar AP LT规范中 建模消息和非建模消息都可以使用LogInfo() API吗?
  • 达芬奇31-40
  • stm32F407 硬件COM事件触发六步换相
  • AI赋能复合材料与智能增材制造:前沿技术研修重磅
  • 智能融合:增材制造多物理场AI建模与工业应用实战
  • 【面向对象】面向对象七大原则
  • linux nfs+autofs
  • 注意点:Git 从安装到分支协作、冲突解决的完整步骤 ---待修改,没看这个步骤,需要重新整理步骤
  • ara::log::LogStream::WithTag的概念和使用案例
  • 跨域场景下的Iframe事件监听
  • Nature Neuroscience | 如何在大规模自动化MRI分析中规避伪影陷阱?
  • Android 开发中,HandlerThread、IntentService 和 AsyncTask区别对比
  • 性能测试终极指南:从指标到实战
  • 《传统企业如何借助数字化转型实现企业增长》
  • 机器学习通关秘籍|Day 03:决策树、随机森林与线性回归
  • 分布式微服务--Nacos持久化
  • Python-机器学习初识
  • 机器学习——集成学习(Ensemble Learning):随机森林(Random Forest),AdaBoost、Gradient Boosting,Stacking
  • 论文阅读笔记:《Curriculum Coarse-to-Fine Selection for High-IPC Dataset Distillation》
  • 2.4 组件通信
  • 高阶 RAG :技术体系串联与实际落地指南​
  • 计算机网络 第2章通信基础(竟成)
  • PYQT的QMessageBox使用示例