LiteOS雲端對接教程01-cJSON組件使用教程

1. JSON與cJSON

JSON —— 輕量級的數據格式

JSON 全稱 JavaScript Object Notation,即 JS對象簡譜,是一種輕量級的數據格式。

它採用完全獨立於編程語言的文本格式來存儲和表示數據,語法簡潔、層次結構清晰,易於人閱讀和編寫,同時也易於機器解析和生成,有效的提升了網絡傳輸效率。

JSON語法規則

JSON對象是一個無序的"名稱/值"鍵值對的集合:

  • 以"{"開始,以"}"結束,允許嵌套使用;
  • 每個名稱和值成對出現,名稱和值之間使用":"分隔;
  • 鍵值對之間用","分隔
  • 在這些字符前後允許存在無意義的空白符;

對於鍵值,可以有如下值:

  • 一個新的json對象
  • 數組:使用"["和"]"表示
  • 數字:直接表示,可以是整數,也可以是浮點數
  • 字符串:使用引號"表示
  • 字面值:false、null、true中的一個(必須是小寫)

示例如下:

{
    "name": "mculover666",
    "age": 22,
    "weight": 55.5
    "address":
    {
        "country": "China",
        "zip-code": 111111
    },
    "skill": ["c", "Java", "Python"],
    "student": false
}

LiteOS中的cJSON組件

cJSON是一個使用C語言編寫的JSON數據解析器,具有超輕便,可移植,單文件的特點,使用MIT開源協議。

LiteOS中已經移植了cJSON,作爲一個組件使用,源碼在sdkIoT_LINK_1.0.0iot_linkcJSON中,其源碼文件只有兩個:

  • cJSON.h
  • cJSON.c

使用的時候,只需要將這兩個文件複製到工程目錄,然後包含頭文件cJSON.h即可,如下:

#include "cJSON.h"

2. cJSON數據結構和設計思想

cJSON的設計思想從其數據結構上就能反映出來。

cJSON使用cJSON結構體來表示一個JSON數據,定義在cJSON.h中,源碼如下:

/* The cJSON structure: */
typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;

    /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
    char *string;
} cJSON;

cJSON的設計很巧妙。

首先,它不是將一整段JSON數據抽象出來,而是將其中的一條JSON數據抽象出來,也就是一個鍵值對,用上面的結構體 strcut cJSON 來表示,其中用來存放值的成員列表如下:

  • String:用於表示該鍵值對的名稱;
  • type:用於表示該鍵值對中值的類型;
  • valuestring:如果鍵值類型(type)是字符串,則將該指針指向鍵值;
  • valueint:如果鍵值類型(type)是整數,則將該指針指向鍵值;
  • valuedouble:如果鍵值類型(type)是浮點數,則將該指針指向鍵值;

其次,一段完整的JSON數據中由很多鍵值對組成,並且涉及到鍵值對的查找、刪除、添加,所以使用鏈表來存儲整段JSON數據,如上面的代碼所示:

  • next指針:指向下一個鍵值對
  • prev指針指向上一個鍵值對

最後,因爲JSON數據支持嵌套,所以一個鍵值對的值會是一個新的JSON數據對象(一條新的鏈表),也有可能是一個數組,方便起見,在cJSON中,數組也表示爲一個數組對象,用鏈表存儲,所以:

在鍵值對結構體中,當該鍵值對的值是一個嵌套的JSON數據或者一個數組時,由child指針指向該條新鏈表。

3. 開啓cJSON組件

在LiteOS中,cJSON組件默認是未開啓的,使用宏定義CONFIG_JSON_ENABLE開啓。

開啓之後,LiteOS會自動進行初始化,並且使用cJSON的內存鉤子將cJSON申請內存的方式變爲使用osalmalloc申請,自動初始化代碼在`linkmain.c`文件中:

cJSON組件自動初始化代碼

4. JSON數據封裝

封裝方法

封裝JSON數據的過程,其實就是創建鏈表和向鏈表中添加節點的過程。

首先來講述一下鏈表中的一些術語:

  • 頭指針:指向鏈表頭結點的指針;
  • 頭結點:不存放有效數據,方便鏈表操作;
  • 首節點:第一個存放有效數據的節點;
  • 尾節點:最後一個存放有效數據的節點;

明白了這幾個概念之後,我們開始講述創建一段完整的JSON數據,即如何創建一條完整的鏈表。

  • ① 創建頭指針:
 cJSON* cjson_test = NULL;

  • ② 創建頭結點,並將頭指針指向頭結點:
cjson_test = cJSON_CreateObject();

  • ③ 盡情的向鏈表中添加節點:
cJSON_AddNullToObject(cJSON * const object, const char * const name);

cJSON_AddTrueToObject(cJSON * const object, const char * const name);

cJSON_AddFalseToObject(cJSON * const object, const char * const name);

cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);

cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);

cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);

cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);

cJSON_AddObjectToObject(cJSON * const object, const char * const name);

cJSON_AddArrayToObject(cJSON * const object, const char * const name);

輸出JSON數據

上面講述,一段完整的JSON數據就是一條長長的鏈表,那麼,如何打印出這段JSON數據呢?

cJSON提供了一個API,可以將整條鏈表中存放的JSON信息輸出到一個字符串中:

(char *) cJSON_Print(const cJSON *item);

使用的時候,只需要接收該函數返回的指針地址即可。

封裝數據和打印數據示例

單純的講述方法還不夠,下面用一個例子來說明,封裝出開頭給出的那段JSON數據。

首先基於HelloWorld工程,創建一個存放示例文件的文件夾cloud_test_demo,並新建一個實驗文件cjson_print_demo.c,編寫如下代碼:

#include <osal.h>
#include <stdio.h>
#include <cJSON.h>

static int cjson_print_demo_entry()
{
    cJSON* cjson_test = NULL;
    cJSON* cjson_address = NULL;
    cJSON* cjson_skill = NULL;
    char* str = NULL;

    /* 創建一個JSON數據對象(鏈表頭結點) */
    cjson_test = cJSON_CreateObject();

    /* 添加一條字符串類型的JSON數據(添加一個鏈表節點) */
    cJSON_AddStringToObject(cjson_test, "name", "mculover666");

    /* 添加一條整數類型的JSON數據(添加一個鏈表節點) */
    cJSON_AddNumberToObject(cjson_test, "age", 22);

    /* 添加一條浮點類型的JSON數據(添加一個鏈表節點) */
    cJSON_AddNumberToObject(cjson_test, "weight", 55.5);

    /* 添加一個嵌套的JSON數據(添加一個鏈表節點) */
    cjson_address = cJSON_CreateObject();
    cJSON_AddStringToObject(cjson_address, "country", "China");
    cJSON_AddNumberToObject(cjson_address, "zip-code", 111111);
    cJSON_AddItemToObject(cjson_test, "address", cjson_address);

    /* 添加一個數組類型的JSON數據(添加一個鏈表節點) */
    cjson_skill = cJSON_CreateArray();
    cJSON_AddItemToArray(cjson_skill, cJSON_CreateString( "C" ));
    cJSON_AddItemToArray(cjson_skill, cJSON_CreateString( "Java" ));
    cJSON_AddItemToArray(cjson_skill, cJSON_CreateString( "Python" ));
    cJSON_AddItemToObject(cjson_test, "skill", cjson_skill);

    /* 添加一個值爲 False 的布爾類型的JSON數據(添加一個鏈表節點) */
    cJSON_AddFalseToObject(cjson_test, "student");

    /* 打印JSON對象(整條鏈表)的所有數據 */
    str = cJSON_Print(cjson_test);
    printf("%s\n", str);

    /* 釋放整條鏈表內存 */
    cJSON_Delete(cjson_test);

    return 0;
}

int standard_app_demo_main()
{
    osal_task_create("cjson_print_demo",cjson_print_demo_entry,NULL,0x800,NULL,2);
    return 0;
}

在user_demo.mk中配置文件路徑:

    #example for cjson_print_demo
    ifeq ($(CONFIG_USER_DEMO), "cjson_print_demo")    
        user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/cloud_test_demo/cjson_print_demo.c}
    endif

位置如下:

添加位置

然後在.sdkconfig中開啓cJSON組件,並且選中該demo:

開啓cJSON組件

實驗結果如圖:

實驗結果

5. cJSON數據解析

解析方法

解析JSON數據的過程,其實就是剝離一個一個鏈表節點(鍵值對)的過程。

解析方法如下:

  • ① 創建鏈表頭指針:
cJSON* cjson_test = NULL;

  • ② 解析整段JSON數據,並將鏈表頭結點地址返回,賦值給頭指針:

解析整段數據使用的API只有一個:

(cJSON *) cJSON_Parse(const char *value);

  • ③ 根據鍵值對的名稱從鏈表中取出對應的值,返回該鍵值對(鏈表節點)的地址
(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);

解析示例

下面用一個例子來說明如何解析出開頭給出的那段JSON數據。

在存放示例文件的文件夾cloud_test_demo中,再新建一個實驗文件cjson_parse_demo.c,編寫如下代碼:

#include <osal.h>
#include <stdio.h>
#include <cJSON.h>

char *message = 
"{                              \
    \"name\":\"mculover666\",   \
    \"age\": 22,                \
    \"weight\": 55.5,           \
    \"address\":                \
        {                       \
            \"country\": \"China\",\
            \"zip-code\": 111111\
        },  \
    \"skill\": [\"c\", \"Java\", \"Python\"],\
    \"student\": false\
}";

static int cjson_test1_demo_entry()
{
   cJSON* cjson_test = NULL;
    cJSON* cjson_name = NULL;
    cJSON* cjson_age = NULL;
    cJSON* cjson_weight = NULL;
    cJSON* cjson_address = NULL;
    cJSON* cjson_address_country = NULL;
    cJSON* cjson_address_zipcode = NULL;
    cJSON* cjson_skill = NULL;
    cJSON* cjson_student = NULL;
    int    skill_array_size = 0, i = 0;
    cJSON* cjson_skill_item = NULL;

    /* 解析整段JSO數據 */
    cjson_test = cJSON_Parse(message);
    if(cjson_test == NULL)
    {
        printf("parse fail.\n");
        return -1;
    }

    /* 依次根據名稱提取JSON數據(鍵值對) */
    cjson_name = cJSON_GetObjectItem(cjson_test, "name");
    cjson_age = cJSON_GetObjectItem(cjson_test, "age");
    cjson_weight = cJSON_GetObjectItem(cjson_test, "weight");

    printf("name: %s\n", cjson_name->valuestring);
    printf("age:%d\n", cjson_age->valueint);
    printf("weight:%.1f\n", cjson_weight->valuedouble);

    /* 解析嵌套json數據 */
    cjson_address = cJSON_GetObjectItem(cjson_test, "address");
    cjson_address_country = cJSON_GetObjectItem(cjson_address, "country");
    cjson_address_zipcode = cJSON_GetObjectItem(cjson_address, "zip-code");
    printf("address-country:%s\naddress-zipcode:%d\n", cjson_address_country->valuestring, cjson_address_zipcode->valueint);

    /* 解析數組 */
    cjson_skill = cJSON_GetObjectItem(cjson_test, "skill");
    skill_array_size = cJSON_GetArraySize(cjson_skill);
    printf("skill:[");
    for(i = 0; i < skill_array_size; i  )
    {
        cjson_skill_item = cJSON_GetArrayItem(cjson_skill, i);
        printf("%s,", cjson_skill_item->valuestring);
    }
    printf("\b]\n");

    /* 解析布爾型數據 */
    cjson_student = cJSON_GetObjectItem(cjson_test, "student");
    if(cjson_student->valueint == 0)
    {
        printf("student: false\n");
    }
    else
    {
        printf("student:error\n");
    }

    /* 釋放整條鏈表內存 */
    cJSON_Delete(cjson_test);
    
    return 0;
}

int standard_app_demo_main()
{
    osal_task_create("cjson_test1_demo",cjson_test1_demo_entry,NULL,0x800,NULL,2);
    return 0;
}

在user_demo.mk中配置文件路徑:

    #example for cjson_parse_demo
    ifeq ($(CONFIG_USER_DEMO), "cjson_parse_demo")    
        user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/cloud_test_demo/cjson_parse_demo.c}
    endif

位置如下:

添加位置

然後在.sdkconfig中開啓cJSON組件,並且選中該demo:

開啓cJSON組件

實驗結果如圖:

實驗結果

注意事項

在本示例中,因爲我提前知道數據的類型,比如字符型或者浮點型,所以我直接使用指針指向對應的數據域提取,在實際使用時,如果提前不確定數據類型,應該先判斷type的值,確定數據類型,再從對應的數據域中提取數據

6. cJSON使用過程中的內存問題

內存及時釋放

cJSON的所有操作都是基於鏈表的,所以cJSON在使用過程中大量的使用malloc從堆中分配動態內存的,所以在使用完之後,應當及時調用下面的函數,清空cJSON指針所指向的內存,該函數也可用於刪除某一條數據:

(void) cJSON_Delete(cJSON *item);

注意:該函數刪除一條JSON數據時,如果有嵌套,會連帶刪除。

內存鉤子

cJSON在支持自定義malloc函數和free函數,方法如下:

  • ① 使用cJSON_Hooks來連接自定義malloc函數和free函數:
typedef struct cJSON_Hooks
{
      /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
      void *(CJSON_CDECL *malloc_fn)(size_t sz);
      void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;

  • ② 初始化鉤子cJSON_Hooks
(void) cJSON_InitHooks(cJSON_Hooks* hooks);
發佈了50 篇原創文章 · 獲贊 2 · 訪問量 5589
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章