Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
cjson
cJSON 避坑笔记
发布于 2024-05-02 12:42:28 浏览:1456
订阅该版
[tocm] # cJSON避坑笔记 ## JSON简介 嵌入式设备与外部通信时,可以使用纯二进制协议以减少通信开销。但是,二进制协议也有一些问题,例如某个信息是不定长的数组,用二进制就不好表示。 使用JSON(JavaScript Object Notation,JavaScript对象表示法)格式可以表示各种各样复杂的数据,目前前端(浏览器)和后端(服务器)之间通信就广泛使用JSON格式。不过,使用JSON格式的消息长度会比二进制格式的大几倍。 ### 基本类型 - 字符串:用双引号包裹:`"abc"` - 数值 - 整型:`30` - 浮点型:`19.99` - 指数形式的浮点型:`1e+6`、`1E-6` > **说明** > > 不支持十六进制(`0xABCD`)。 - 布尔:`true`或`false` - null:`null` ### 对象 `{"属性名":属性值}`。如果有多对,它们之间用一个逗号分隔,逗号不能多(最后一对的后面不能有逗号)也不能少。`{`、`}`、`"属性名"`、`:`、`属性值`、`,`的左右可以有一或多个空白符(空格、回车`\r`、换行`\n`、制表符`\t`)。 - `{}` - `{"a":"abc"}` - `{"a": "abc", "b": 19.99, "c": true, "d": false, "e": null}` > **说明** > > 对象内(同一层)的属性名不能重复。 ### 数组 `[基本类型或对象]`如果有多个元素,它们之间用一个逗号分隔,逗号不能多(最后一对的后面不能有逗号)也不能少。`[`、`基本类型或对象`、`]`的左右可以有一或多个空白符。 - `[]` - `[1, 1, 3]` - `[{"a": "abc", "b": 19.99, "c": true, "d": false, "e": null}, "abc", 19.99, true, false, null]` ### 嵌套 - 数组内可以嵌套对象:`["a", {"a": "b"}]` - 数组内可以嵌套数组:`[1, ["a", "b"]]` - 对象内不能直接嵌套数组,应加上属性名:`{"a": ["b", "c"]}`。 - 对象内不能直接嵌套对象,应加上属性名:`{"a": 1, "b": {"c": 2}}` ### 编码 如果字符串使用了非ASCII字符(例如中文、特殊符号),为了避免另一端(上位机、前端或后端)乱码,应使用UTF-8编码格式,而不是GBK或GB2312格式。 对象中的属性名在其它编程语言中可能作为类中的成员名称以便于解析和读取,不建议属性名用非ASCII字符。 ## cJSON结构体 > cJSON是一个用C语言实现的JSON序列化(构造JSON)和反序列化(解析JSON)库。 > > 仓库地址:
,通常只需要其中的`cJSON.h`和`cJSON.c`文件。 ```c /* * cJSON类型 */ #define cJSON_Invalid (0) /** 无效的cJSON项 */ #define cJSON_False (1 << 0) /** false */ #define cJSON_True (1 << 1) /** true */ #define cJSON_NULL (1 << 2) /** null */ #define cJSON_Number (1 << 3) /** 数值 */ #define cJSON_String (1 << 4) /** 字符串 */ #define cJSON_Array (1 << 5) /** 数组 */ #define cJSON_Object (1 << 6) /** 对象 */ typedef struct cJSON { /** 对象或数组的前一项 */ struct cJSON *next; /** 对象或数组的后一项 */ struct cJSON *prev; /** 对象或数组的子元素 */ struct cJSON *child; /** cJSON项的类型 */ int type; /** 字符串的值(如果类型为字符串) */ char *valuestring; /** 已过时的成员,不建议使用 */ int valueint; /** 数值的值(如果类型为数值) */ double valuedouble; /* 对象子项的属性名 */ char *string; } cJSON; ``` 可以猜测,一个cJSON结构体可以表示字符串、数值、布尔、`null`、对象(或对象的一个属性名和属性值)、数组之一,通过prev、next、child成员链接到同一级或更深的一级的cJSON项。 cJSON的API函数名中很多都有“Item”(例如`cJSON_GetObjectItem`、`cJSON_AddItemToObject`),是指对象或数组的一级子项,本文称为“元素”。 ## 解析 以解析以下对象为例: ```json { "type": "request", "id": 123, "crypt": false, "params": { "cmd": 1 } } ``` --- - 应检查解析结果,如果为NULL则不执行后续正常流程: ```c retval_t parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { LOG_E("failed to parse the message to JSON"); goto fail; } // ... fail: // ... } ``` 其中,root_object表示JSON最外层的对象`{}`,本文称为“根对象”。 - 使用完了解析到的cJSON对象后应调用`cJSON_Delete`删除对象以释放内存: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); // ... cJSON_Delete(root_object); } ``` - `cJSON_Parse`不仅能解析对象、数组,也能解析基本类型: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (!cJSON_IsObject(root_object)) { LOG_E("failed to parse the message to JSON object"); // ❌错误示范,就算是基本类型也需要使用cJSON_Delete来释放 return; } // ... cJSON_Delete(root_object); } ``` - 不要直接操作cJSON结构体的成员: ```c // ❌错误示范 void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } cJSON* item; item = cJSON_GetObjectItem(root_object, "type"); char *type = item->valuestring; item = cJSON_GetObjectItem(root_object, "id"); double id = item->valuedouble; item = cJSON_GetObjectItem(root_object, "crypt"); int crypt = !!(item->type & cJSON_False); // ... end: // ... } ``` 因为,传入的`string`的JSON里(例如`{}`)可能没有对应的字段,那么调用`cJSON_GetObjectItem`返回的`item`可能为NULL。在C语言里访问一个NULL的成员是极其不安全,可能会导致硬件错误,可能会被内存保护单元禁止,可能到程序直接崩溃、重启;如果芯片的架构没有对应的保护,可能暂时没有问题,后续会出现一些莫名奇妙的BUG。 也不要像网上某些博客那样,判断对应的`item`不为NULL了就操作cJSON结构体的成员: ```c // ❌错误示范 void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } cJSON* type_item = cJSON_GetObjectItem(root_object, "type"); if (type_item != NULL) { char *type = type_item->valuestring; } cJSON* item = cJSON_GetObjectItem(root_object, "id"); if (item != NULL) { double id = item->valuedouble; } item = cJSON_GetObjectItem(root_object, "crypt"); if (item != NULL) { int crypt = item->type & cJSON_False; } // ... end: // ... } ``` 因为,传入的`string`的JSON里对应的属性值的类型可能不对应,例如`{"type": 123}`,访问`type_item->valuestring`是无效的。 不要假设从(物理)外部传入的参数的类型不会错。如果是串口这类传输,传输过程和分割消息逻辑是可能出错的;即使是http、mqtt这类无差错的协议,提供消息的(上位机/服务器开发者?)也可能出BUG。 `cJSON_Is*`这类函数在传入的元素不为NULL且类型匹配时返回真(1),否则返回假(0): ```c typedef int cJSON_bool; cJSON_bool cJSON_IsFalse(const cJSON * const item); cJSON_bool cJSON_IsTrue(const cJSON * const item); cJSON_bool cJSON_IsBool(const cJSON * const item); cJSON_bool cJSON_IsNull(const cJSON * const item); cJSON_bool cJSON_IsNumber(const cJSON * const item); cJSON_bool cJSON_IsString(const cJSON * const item); cJSON_bool cJSON_IsArray(const cJSON * const item); cJSON_bool cJSON_IsObject(const cJSON * const item); ``` 应在确保属性值的类型是对应的类型再进一步获取: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } cJSON *item; item = cJSON_GetObjectItem(root_object, "type"); if (cJSON_IsString(item)) { char *type = item->valuestring; } item = cJSON_GetObjectItem(root_object, "id"); if (cJSON_IsNumber(item)) { double id = item->valuedouble; } item = cJSON_GetObjectItem(root_object, "crypt"); if (cJSON_IsBool(item)) { int crypt = item->type & cJSON_False; } // ... end: // ... } ``` 上面的代码也不优雅,从某种程序上降低了type、id、crypt的作用域,如果把变量定义在判断的外面则需要初始化一个默认值。我们应该假设我们不清楚cJSON内部对cJSON结构体的成员是怎么使用的。统一用函数来操作: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { return; } char *type = cJSON_GetStringValue(cJSON_GetObjectItem(root_object, "type")); if (type == NULL) { goto end; } double id = cJSON_GetNumberValue(cJSON_GetObjectItem(root_object, "id")); if (isnan(id)) { goto end; } int crypt = 默认值; cJSON* crypt_item = cJSON_GetObjectItem(root_object, "crypt"); if (cJSON_IsBool(crypt_item)) { crypt = cJSON_IsTrue(crypt_item); } // 如果期望的id是整型 int id_int = (int)id; // ... end: // ... } ``` 在JSON中,整数和浮点数的类型都是数值(number);在cJSON中,整数和浮点数的类型都是double。其中,`cJSON_GetNumberValue`内部对传入的参数为NULL时返回`NAN`(not a number),用`
`中的`isnan`函数可以判断double类型的(根据[IEEE754](https://zh.wikipedia.org/wiki/IEEE_754),`NAN`的二进制值不是唯一的,所以不能用`if (id == NAN) { }`来判断一个浮点数是否为`NAN`)。 - 不要直接将cJSON项解析为int,因为(如果`cJSON_GetObjectItem`返回NULL,`cJSON_GetNumberValue(NULL)`返回`NAN`)将`NAN`强制转换为int类型的结果是未定义的: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); // ❌错误示范 int id = cJSON_GetNumberValue(cJSON_GetObjectItem(root_object, "id")); // ... } ``` - 不要直接使用`cJSON_GetStringValue`返回的`char *`变量: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); char *type = cJSON_GetStringValue(cJSON_GetObjectItem(root_object, "type")); // ❌错误示范 printf("type: %s\n", type); // ❌错误示范 if (strcmp(type, "request") == 0) { // ... } // ... } ``` 因为标准库的很多关于字符串的函数是不那么安全的,例如在esp32中用`%s`打印一个为NULL的字符串指针将导致程序崩溃,strcmp也不允许传入的参数为NULL。应先检查`cJSON_GetStringValue`函数返回的`char *`是否为NULL。 ## 构造 以构造以下对象为例: ```json { "type": "request", "id": 123, "crypt": false, "params": { "cmd": 1 } } ``` --- - 使用完了创建的cJSON对象后应调用`cJSON_Delete`删除对象以释放内存: ```c void construct_object(void) { cJSON *root_object = cJSON_CreateObject(); // ... cJSON_Delete(root_object); } ``` 为了防止遗忘,建议在写下cJSON_CreateObject后就先写好cJSON_Delete。cJSON_Delete会递归删除所有子元素,所以只需要删除根对象。 - 如果元素是基本类型,优先使用以下函数而不是`cJSON_AddItemToObject(object, key, cJSON_CreateXxx())` ```c /* * 成功添加时返回添加的元素,失败时返回NULL */ cJSON* cJSON_AddNullToObject(cJSON * const object, const char * const name); cJSON* cJSON_AddTrueToObject(cJSON * const object, const char * const name); 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); ``` 因为使用这类函数如果添加失败,函数内部会将创建的元素的内存释放,所以可以放心的添加元素。在已经检查过第一个参数object不为NULL的情况下使用这类函数,不必判断添加是否成功: ```c cJSON* root_object = cJSON_CreateObject(); if (root_object == NULL) { goto end; } cJSON_AddStringToObject(root_object, "type", "request"); cJSON_AddNumberToObject(root_object, "id", 123); cJSON_AddBoolToObject(root_object, "crypt", false); // ... end: // ... ``` - 如果元素是对象或数组,应在添加失败时释放待添加的元素: ```c /* * 成功添加时返回true,失败时返回false */ cJSON_bool cJSON_AddItemToArray(cJSON *array, cJSON *item); cJSON_bool cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); void construct_object(void) { cJSON* root_object = cJSON_CreateObject(); // 添加元素到root_object... cJSON* params = cJSON_CreateObject(); // 添加元素到params... if (!cJSON_AddItemToObject(root_object, "params", params)) { cJSON_Delete(params); } cJSON_Delete(root_object); } ``` 因为如果添加params到根元素失败,使用`cJSON_Delete(root_object)`删除根对象时params并不会被释放。 - 向对象添加重复添加相同属性名的元素,结果是对象中存在多个相同属性名的元素 ```c cJSON* root_object = cJSON_CreateObject(); cJSON_AddStringToObject(root_object, "key", "value1"); cJSON_AddStringToObject(root_object, "key", "value2"); char *object_string = cJSON_Print(root_object); if (object_string != NULL) { printf("%s\n", object_string); } // ... ``` 打印结果是: ```json { "key": "value1", "key": "value2" } ``` 这并不符合JSON规范。假设使用cJSON作为数据结构存储数据,可能反复添加相同属性名的元素,应做去重处理: ```c /** * @brief 设置的对象 */ static cJSON *s_settings_object; /** * @brief 更新设置中的字符串 * * @note 如果属性不存在则添加属性,如果属性已存在则替换 * * @param key 属性名 * @param value 新的属性值 * * @return 0表示成功,其它表示失败 */ int app_settings_update_string(char *key, char *value) { cJSON *item = cJSON_CreateString(value); if (cJSON_HasObjectItem(s_settings_object, key)) { cJSON_DeleteItemFromObject(s_settings_object, key); } if (cJSON_AddItemToObject(s_settings_object, key, item)) { // ... return 0; } cJSON_Delete(item); return -1; } // ... ``` - 使用创建Number相关的函数时(例如`cJSON_AddNumberToObject`) ,传入的number的取值范围是`[INT_MIN, INT_MAX]`(在32位环境下是`[-2147483648, 2147483647]`),如果超出范围则被限制在临界值。 ## 遍历 - 在cJSON中遍历对象或数组都是使用`cJSON_ArrayForEach`来遍历,遍历的本质是一直访问cJSON的next成员直到为NULL。用过双链表的应该知道,通常有`list_for_each_xxx`和`list_for_each_xxx_safe`两个方法,用不带safe的方法遍历的时候,如果遍历过程中删除了当前节点,则遍历会崩溃,因为破坏了循环使用的链表信息。`cJSON_ArrayForEach`是不安全的方法,因此在遍历过程中不应对元素进行增、删、改操作: ```c // ❌错误示范 void test_cjson(void) { cJSON *root_object = cJSON_CreateObject(); cJSON_AddStringToObject(root_object, "type", "request"); cJSON_AddNumberToObject(root_object, "id", 123); cJSON_AddBoolToObject(root_object, "crypt", false); cJSON *item; cJSON_ArrayForEach(item, root_object) { printf("item key: %s\n", item->string); if (strcmp(item->string, "id") == 0) { cJSON_ReplaceItemInObject(root_object, item->string, cJSON_CreateString("new value")); } } cJSON_ArrayForEach(item, root_object) { printf("item key: %s\n", item->string); if (strcmp(item->string, "id") == 0) { cJSON_DeleteItemFromObject(root_object, item->string); } } cJSON_ArrayForEach(item, root_object) { printf("item key: %s\n", item->string); if (strcmp(item->string, "crypt") == 0) { cJSON_AddStringToObject(root_object, "key", "value"); } } // ... } ``` 以删除元素为例,如果只需要删除一个元素,直接在循环外删除即可。如果要删除多个元素,可以遍历待删除的属性名来删除: ```c void test_cjson(void) { cJSON *root_object = cJSON_CreateObject(); cJSON_AddStringToObject(root_object, "type", "request"); cJSON_AddNumberToObject(root_object, "id", 123); cJSON_AddBoolToObject(root_object, "crypt", false); char *keys[] = {"type", "id"}; for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { cJSON_DeleteItemFromObject(root_object, keys[i]); } // ... } ``` ## 安全检查 - 在程序崩溃了无数次后,可能会动不动就添加NULL判断,使程序变得不那么优雅。实际上,据我分析,除了`cJSON_free`函数以外(因为它直接调用了内存释放函数),cJSON的方法(函数或宏定义)都对NULL做了处理,在调用这些方法时没必要添加NULL判断。例如: ```c void parse_object(char *string) { if (string != NULL) // 多余的 { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } cJSON *type_item = cJSON_GetObjectItem(root_object, "type"); if (type_item != NULL) // 多余的 { char *type = cJSON_GetStringValue(type_item); if (type == NULL) { goto end; } } cJSON *id_item = cJSON_GetObjectItem(root_object, "id"); if (id_item != NULL) // 多余的 { double id = cJSON_GetNumberValue(id_item); if (isnan(id)) { goto end; } } bool crypt = false; cJSON *crypt_item = cJSON_GetObjectItem(root_object, "crypt"); if (crypt_item != NULL) // 多余的 { if (cJSON_IsBool(crypt_item)) { crypt = cJSON_IsTrue(crypt_item); } } cJSON *params_object = cJSON_GetObjectItem(root_object, "params"); if (params_object != NULL) // 多余的 { double cmd = cJSON_GetNumberValue(cJSON_GetObjectItem(params_object, "cmd")); } end: if (root_object != NULL) // 多余的 { cJSON_Delete(root_object); } } } void construct_object(void) { cJSON *root_object = cJSON_CreateObject(); if (root_object == NULL) { goto end; } cJSON_AddStringToObject(root_object, "type", "request"); cJSON_AddNumberToObject(root_object, "id", 123); cJSON_AddBoolToObject(root_object, "crypt", false); cJSON *params_object = cJSON_CreateObject(); if (params_object != NULL) // 多余的 { cJSON_AddItemToObject(root_object, "params", params_object); } char *object_string = cJSON_Print(root_object); if (object_string != NULL) { // ... cJSON_free(object_string); } end: cJSON_Delete(root_object); } void traverse_object_1(cJSON *object) { if (object != NULL) // 多余的 { cJSON *item; cJSON_ArrayForEach(item, object) { // ... } } } void traverse_object_2(cJSON *object) { if (cJSON_IsObject(object)) // 多余的,除非需要区分object是对象还是数组 { cJSON *item; cJSON_ArrayForEach(item, object) { // ... } } } ``` - 对象中或数组没有的元素,替换或删除是无副作用的,没必要检查元素是否存在。例如: ```c static void test_cjson(void) { cJSON *root_object = cJSON_CreateObject(); cJSON_AddStringToObject(root_object, "type", "request"); cJSON_AddNumberToObject(root_object, "id", 123); cJSON_AddBoolToObject(root_object, "crypt", false); char *keys[] = {"type", "id"}; for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { cJSON *item = cJSON_GetObjectItem(root_object, keys[i]); if (item != NULL) // 多余的 { cJSON_DeleteItemFromObject(root_object, keys[i]); } } for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { if (cJSON_HasObjectItem(root_object, keys[i])) // 多余的 { cJSON_DeleteItemFromObject(root_object, keys[i]); } } for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { if (cJSON_HasObjectItem(root_object, keys[i])) // 多余的 { cJSON_ReplaceItemInArray(...); } } // ... } ``` - 调用`cJSON_xxx`时可以不检查参数是否为NULL,但返回值如果不调用`cJSON_xxx`了,该检查还是要检查。例如: ```c char *type = cJSON_GetStringValue(cJSON_GetObjectItem(object, "type")); if (type == NULL) { // ... } else { // ... } ``` ## 内存处理 一个cJSON结构体大约占用40个字节,根对象和数组是链接了多个cJSON结构体的。如果使用完不释放内存,那么可用内存将越来越少,最终程序崩溃。 - 使用`cJSON_Parse`、`cJSON_CreateObject`、`cJSON_CreateArray`等产生的根元素,用完后要调用`cJSON_Delete`释放。 - 使用`cJSON_Print`、`cJSON_PrintUnformatted`、`cJSON_PrintBuffered`、`cJSON_PrintPreallocated`产生的字符串,用完后要调用`cJSON_free`释放。 - 不要重复释放根对象或数组: ```c // ❌错误示范 void handle_request(cJSON *req_object) { cJSON_Delete(req_object); } void handle_request_message(char *string) { cJSON *req_object = cJSON_Parse(string); if (req_object == NULL) { goto end; } // ... handle_request(req_object); end: // 程序崩溃 cJSON_Delete(req_object); } ``` - 不要释放已经添加到根对象或数组中的元素: ```c void handle(cJSON *params_object) { // ... // ❌错误示范(虽然释放了内存,但破坏了root_object的链表) cJSON_Delete(params_object); // ❌无意义的(这样只是将局部变量的值赋为NULL,root_object中对应的指针并不会变为NULL) params_object = NULL; } void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } // ... cJSON *params_object = cJSON_GetObjectItem(root_object, "params"); handle(params_object); // ... end: // 程序崩溃 cJSON_Delete(root_object); } ``` - 不要在根元素的生命周期外使用指针类型的元素或其指针类型的子元素: ```c /** * @brief 耗时服务线程 * * @param params cJSON的指针 */ void service_thread(void *params) { // ... } /** * @brief 处理耗时请求 * * @param params 请求参数 */ void handle_service_request(cJSON *params_object) { // ❌错误示范 rt_thread_t thread = rt_thread_create("thread", service_thread, params_object, 1024, 10, 10); if (thread != RT_NULL) { rt_thread_startup(thread); } } /** * @brief 处理请求消息 * * @param string 消息字符串 */ void handle_request_message(char *string) { cJSON *req_object = cJSON_Parse(string); if (req_object == NULL) { goto parse_end; } char *type = cJSON_GetStringValue(cJSON_GetObjectItem(req_object, "type")); if (type == NULL) { goto parse_end; } double id = cJSON_GetNumberValue(cJSON_GetObjectItem(root_object, "id")); if (isnan(id)) { goto end; } int crypt = cJSON_IsTrue(cJSON_GetObjectItem(root_object, "crypt")); cJSON *params_object = cJSON_GetObjectItem(req_object, "params"); handle_request(params_object); parse_end: cJSON_Delete(req_object); // 应答外部... // ❌错误示范 if (type != NULL) { printf("message type: %s\n", type); } // 非指针类型的元素(id、crypt)可以继续使用... } ``` - 不要将已经添加到根对象或数组中的cJSON指针类型的元素添加到其它根对象或数组中: ```c void handle_request_message(char *string) { cJSON *req_object = cJSON_Parse(string); if (req_object == NULL) { goto req_end; } cJSON *id_item = cJSON_GetObjectItem(req_object, "id"); if (!cJSON_IsString(id_item)) { goto req_end; } char *id = cJSON_GetStringValue(id_item); cJSON *params = cJSON_GetObjectItem(req_object, "params"); // 应答外部 cJSON *res_object = cJSON_CreateObject(); if (res_object == NULL) { goto res_end; } // ❌错误示范 cJSON_AddItemToObject(res_object, "id", id_item); // ❌错误示范 cJSON_AddItemToObject(res_object, "params", params); // ... res_end: cJSON_Delete(res_object); req_end: // 删除res_object时已经释放了id_item和params,此处导致程序崩溃 cJSON_Delete(req_object); } ``` 应使用会创建元素副本的函数,或手动创建副本来添加到其它根元素: ```c void handle_request_message(char *string) { cJSON *req_object = cJSON_Parse(string); if (req_object == NULL) { goto end; } cJSON *id_item = cJSON_GetObjectItem(req_object, "id"); if (!cJSON_IsString(id_item)) { goto end; } char *id = cJSON_GetStringValue(id_item); // 应答外部 cJSON *res_object = cJSON_CreateObject(); // 方式1 cJSON_AddStringToObject(res_object, "id", id); // 方式2 cJSON_AddItemToObject(res_object, "id", cJSON_CreateString(id)); // 方式3 cJSON_AddItemToObject(res_object, "id", cJSON_Duplicate(id_item, 1)); // ... cJSON_Delete(res_object); end: cJSON_Delete(req_object); } ``` - 重复释放或遗漏释放都是极其不安全的,所以存在多层调用传递cJSON元素时,应明确由调用方还是被调用的函数负责释放。正常来说,在哪个函数产生的就应该在哪个函数释放。如果不是,应该注明。 ```c /** * @brief 向上位机发出上报消息 * * @param report_object 上报对象。此函数不释放,应由调用方负责释放 */ void app_message_send_report(cJSON *report_object) { int cmd = cJSON_GetNumberValue(cJSON_GetObjectItem(report_object, "cmd")); if (cmd == 1) { // 特殊处理... } char *report_string = cJSON_PrintUnformatted(report_object); cJSON_free(report_string); } /** * @brief 本设备向上位机上报消息 * * @param cmd 指令码 * @param params 上报参数对象。应由调用方申请,由此函数负责释放 * * @retval 0 成功 * @return -1 当前条件不允许上报 */ int app_protocol_report(int cmd, cJSON *params) { if (预检环境或条件不通过) { // ... cJSON_Delete(params); return -1; } cJSON *report_object = cJSON_CreateObject(); cJSON_AddNumberToObject(report_object, "cmd", 1); cJSON_AddItemToObject(report_object, "params", params); // ... cJSON_Delete(report_object); return 0; } void main(void) { // ... cJSON *params = cJSON_CreateObject(); // 添加参数项... app_protocol_report(1, params); } ``` 这个例子中,在调用`app_protocol_report`函数的函数中释放`params`是不合适的,因为在调用`app_protocol_report`时,如果失败则`params`需要单独释放,如果成功则`params`被添加到了某个根对象(不用单独释放)。使用规范的Doxygen注释后,在调用时就比较清楚: ![screenshot_1714580736095.png](https://oss-club.rt-thread.org/uploads/20240502/e6fc98486c6a5055ff7620b65f975f8b.png) - 如果函数的参数是cJSON对象或数组,建议注明是否可以传入NULL: ```c /** * @brief 本设备向上位机上报消息 * * @param cmd 指令码 * @param params 上报参数对象,可传入NULL表示不需要参数 */ void app_protocol_report(cJSON *params) { cJSON *report_object = cJSON_CreateObject(); cJSON_AddItemToObject(report_object, "params", params); // ... cJSON_Delete(report_object); } ``` > **说明** > > 调用`cJSON_Xxx`返回失败的结果大概率是因为没有正确(使用`cJSON_Delete`和`cJSON_free`)释放内存,导致函数申请内存失败。其实,如果代码经过充分的测试,确认没有内存泄露,有些检查是不必要的: > > ```c > void construct_object(void) > { > cJSON* root_object = cJSON_CreateObject(); > > // 不是必要的 > if (root_object == NULL) > { > goto end; > } > > // ... > > cJSON* params = cJSON_CreateObject(); > // ... > int is_success = cJSON_AddItemToObject(root_object, "params", params); > > // 不是必要的 > if (!is_success) > { > cJSON_Delete(params); > } > > char *object_string = cJSON_Print(root_object); > > // 不是必要的(如果root_object被添加的元素的数量是可控的) > if (object_string == NULL) > { > goto end; > } > > cJSON_free(object_string); > > end: > cJSON_Delete(root_object); > } > ``` ## 线程安全 > 如官方的说明文档所述,cJSON在满足以下条件时操作cJSON结构体是线程安全的: > > - 永远不会使用cJSON_GetErrorPtr(可以使用cJSON_ParseWithOpts的return_parse_end参数); > - cJSON_InitHooks只在任何线程中使用cJSON之前调用; > - 在所有对cJSON函数的调用返回之前,永远不会调用setlocale。 - 但是,传输JSON数据时应该对单条完整的消息做互斥处理: ```c static pthread_mutex_t s_mutex = PTHREAD_MUTEX_INITIALIZER; /** * @brief 发送应答消息 * * @param string 应答消息字符串 */ void app_send_message(char *string) { pthread_mutex_lock(&s_mutex); // 发送... pthread_mutex_unlock(&s_mutex); } /** * @brief 处理请求消息并应答 * * @param string 请求消息字符串 */ void handle_request_message(char *string) { cJSON *req_object = cJSON_Parse(string); if (req_object == NULL) { goto req_end; } // 处理请求 // 应答请求 cJSON *res_object = cJSON_CreateObject(); // ... // 发送应答 cJSON *res_string = cJSON_PrintUnformatted(res_object); cJSON_free(res_string); res_end: cJSON_Delete(res_object); req_end: // 删除res_object时已经释放了id_item和params,此处导致程序崩溃 cJSON_Delete(req_object); } ``` ## 消息分割 通常,通信协议的请求和应答的根元素是对象,即一个大的对象表示一条完整的消息。在串口这类不那么可靠的传输中,可能漏传、多传、错传,可能连续收到多条消息合在一起在环形缓冲区,需要分割消息,否则无法保证接收到的数据是完整的。一个简单的思路是每读到`{`使计数变量加1,每读到`}`使计数变量减1;同时,每读读到第一个`{`(计数变量从0变到1)表示一条消息的开始,计数变量从正数变回0表示一条消息的结束: ```c /** * @brief 对象读取器 */ typedef struct app_protocol_object_reader { /** 大括号计数 */ size_t brace_count; /** 已接收到的字节数 */ size_t recv_index; /** 接收缓冲区*/ char *recv_buffer; /** 接收缓冲区的大小 */ size_t recv_buffer_size; /** 对象的总字节数 */ size_t recv_nbytes; /** 开始接收对象的秒数 */ uint32_t recv_object_begin_seconds; } app_protocol_object_reader_t; /** * @brief 读取JSON对象 * * @param reader 读取器 * * @return 是否已接收到一个完整的对象 */ bool app_protocol_read_object(app_protocol_object_reader_t *reader) { char recv_byte = reader->recv_buffer[reader->recv_index]; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); if (recv_byte == '{') { reader->brace_count++; if (reader->brace_count == 1) { reader->recv_object_begin_seconds = ts.tv_sec; } } else if (reader->brace_count == 0) { M_LOG_D("字符%c被丢弃", recv_byte); reader->recv_index = 0; } if (reader->brace_count > 0) { reader->recv_index++; if (recv_byte == '}') { reader->brace_count--; if (reader->brace_count == 0) { reader->recv_buffer[reader->recv_index] = '\0'; reader->recv_nbytes = reader->recv_index; reader->recv_index = 0; return true; } } if (ts.tv_sec - reader->recv_object_begin_seconds > 2) { M_LOG_W("等待完整数据包超时"); reader->recv_index = 0; reader->brace_count = 0; reader->recv_object_begin_seconds = ts.tv_sec; } } if (reader->recv_index == reader->recv_buffer_size) { reader->recv_index = 0; reader->brace_count = 0; M_LOG_E("消息长度超过最大值"); } return false; } /** * @brief 串口接收线程 * * @param params app_uart_num_t */ static void *uart_recv_thread(void *params) { app_uart_num_t uart_num = (app_uart_num_t)params; app_protocol_object_reader_t reader = { .recv_buffer = (char *)m_malloc(APP_MSG_MAX_NBYTES + 1), .recv_buffer_size = APP_MSG_MAX_NBYTES, .brace_count = 0, .recv_index = 0, }; while (true) { if (app_uart_read_byte(uart_num, &reader.recv_buffer[reader.recv_index], 1000)) { if (app_protocol_read_object(&reader)) { M_LOG_D("从%s接收到%dbytes", app_get_uart_name(uart_num), reader.recv_nbytes); handle_request_message(reader.recv_buffer); } } } return NULL; } ``` ## 版本差异 在MDK pack installer中安装,cJSON的版本是v1.7.7。与v1.7.15以上的版本相比,v1.7.7少了一些安全检查,没有cJSON_GetNumberValue函数。如果可以,建议使用最新的版本。 如果使用旧版本,cJSON_GetNumberValue可以从v1.7.15的cJSON.c文件中复制出来: ```c #include
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) { if (!cJSON_IsNumber(item)) { return (double) NAN; } return item->valuedouble; } ``` 或者使用cJSON_GetObjectItem: ```c void parse_object(char *string) { cJSON *root_object = cJSON_Parse(string); if (root_object == NULL) { goto end; } cJSON* item = cJSON_GetObjectItem(root_object, "id"); if (cJSON_IsNumber(item)) { double id = item->valuedouble; } else { // ... } // ... end: // ... } ```
5
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
聚散无由
https://jswyll.com/
文章
1
回答
10
被采纳
5
关注TA
发私信
相关文章
1
pandora开发板使用cjson,内存不足。
2
cjson 安装后编译 出现报错
3
cjson软件包编译出现警告
4
阿里云控制台发送到板子上的数据存放在哪里?
5
rt_cjson_tool 创建json字符串问题?
6
哪位大神用webnet向网页直接返回json字符串?
7
rtthread最新版的cjson软件包打印构建好的数据是NULL?
8
mqtt的cjson中如何获取多个键值对?
9
cJSON、kawaii-mqtt结合使用示例
10
cJSON解析内存占用极高,有没有解决办法?
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
ulog
C++_cpp
at_device
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
8
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
5
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部