cJson 學習筆記

2022-11-20 15:03:41

cJson 學習筆記

一、前言

思考這麼一個問題:對於不同的裝置如何進行資料交換?可以考慮使用輕量級別的 JSON 格式。

那麼需要我們手寫一個 JSON 解析器嗎?這大可不必,因為已經有前輩提供了開源的輕量級的 JSON 解析器——cJSON。我們會用就可以了,當然你也可以深入原始碼進行學習。

下圖則向我們展示瞭如何通過 cJSON 實現 Client 與 Server 的資料交換:

  • Client 在傳送資料之前,通過 cJSON 將自己的專屬資料格式 Data_ClientFormat 轉化為了通用格式 JSON
  • 伺服器端在收到 JSON 資料後,通過 cJSON 將其轉化為伺服器端的專屬資料格式 Data_ServerFormat
  • 反之同理

在介紹 cJSON 之前,先來對 JSON 這個資料格式有個簡單瞭解。

二、JSON 簡介

1.1 什麼是 JSON

JSON 指的是 JavaScript 物件表示法(JavaScript Object Notation)。但它並不是程式語言,而是一種可以在伺服器和使用者端之間傳輸的資料交換格式

1.2 JSON 結構

JSON 的兩種資料結構:

  1. 物件:A collection of key/value pairs(一個無序的 key / value 對的集合)
  2. 陣列:An ordered list of values(一 / 多個 value 的有序列表)

從上述描述中,我們可以獲得如下四種資訊:

  • 物件(Object)
  • 陣列(Array)
  • 鍵(key)
  • 值(Value)

1.3 JSON 物件

JSON 物件具體格式如下圖所示:

  • 一個物件以{開始,以}結束,是若干「key / value 對」的集合
  • key 和 value 使用:分隔
  • key / value 對之間使用,分隔

注意事項:

  1. 鍵:必須是 string 型別
  2. 值:可以是合法的 JSON 資料型別(字串、數值、物件、陣列、布林值或 null)

如,這是一個合法的 JSON 物件:

{
    "name" : "張三"
}

這也是一個合法的 JSON 物件:

{
    "name" : "張三",
    "age"  : 18,
    "sex"  : "男"
}

1.4 JSON 陣列

JSON 陣列具體格式如下圖所示:

  • 一個陣列以[開始,]結束,是若干 value 的有序集合
  • 多個 value 以,分隔

如,這是一個合法的 JSON 陣列:

[
    "張三",
    18,
    "男"
]

該陣列包含三個 value,分別為 string、number、string

這也是一個合法的 JSON 陣列:

[
    {
        "name"	: "張三",
        "age"	: 18,
        "sex"	: "男"
    },
    {
        "name"	: "李四",
        "age"	: 19,
        "sex"	: "男"
    }
]

該陣列包含兩個 Object,每個 Object 又包含若干 key / value。

1.5 JSON 值

值(value)可以是:

  • 字串:必須用雙引號括起來
  • 數 值:十進位制整數或浮點數
  • 對 象:鍵 / 值對的集合
  • 數 組:值的集合
  • 布林值:true 或 false
  • null

value 可以是簡單的用雙引號引起來的 string 串,也可以是一個數值;或布林值(true or false),或 null。

當然也可以是複雜的 object 或 array,這些取值是可以巢狀的。

在「1.4 JSON 陣列」中,第二個例子就是一個巢狀的舉例,當然也可以這麼巢狀:

{
    "class_name"	: "計科一班",
    "student_num"	: 2,
    "student_info"	: 
    [
        {
            "name"	: "張三",
            "age"	: 18,
            "sex"	: "男"
        },
        {
            "name"	: "李四",
            "age"	: 19,
            "sex"	: "男"
        }
    ]
}

三、cJSON 使用教學

3.1 cJSON 使用說明

原始碼下載:https://www.aliyundrive.com/s/vms4mGLStGm

編譯環境:CentOS 7

原始碼中包含 cJSON.h 和 cJSON.c,以及一個測試程式 main.c,測試程式的功能是輸出一個 JSON 字串:

{
    "name": "張三",
    "age":  18,
    "sex":  "男"
}

你可以通過下面兩種方法執行該程式:

  1. $ gcc *.c -o main -lm,會生成一個可執行檔案 main,執行該檔案即可
  2. 將 cJSON.c 打包成靜態庫 / 動態庫,然後在編譯 main.c 的時候將其連結上就可以了

由於原始碼中使用了 pow、floor 函數,所以在編譯的時要連結上 math 庫,也就是 -lm 指令。

如果在編譯過程中報好多 warning(如下圖所示)警告:

不要慌,這並不影響程式的執行,如果你想消除這些警告,不妨將 cJSON.c 格式化一下(用 VSCode 按下alt+shift+F)。

至於原理,不妨參考這篇文章:gcc編譯警告關於(warning: XXX...[-Wmisleading-indentation] if(err)之類的問題)

3.2 cJSON structure

首先,我們先對 cJSON 的結構體有個初步瞭解,其定義如下:

typedef struct cJSON
{
  struct cJSON *next, *prev;
  struct cJSON *child;

  int type;

  char *valuestring;
  int valueint;
  double valuedouble;

  char *string;
} cJSON;
  • type:用於區分 JSON 型別
    • 0 表示 false
    • 1 表示 true
    • 2 表示 null
    • 3 表示 number
    • 4 表示 string
    • 5 表示 array
    • 6 表示 object
  • string :代表「鍵 / 值對」的鍵
  • value*:代表「鍵 / 值對」的值,搭配 type 使用
    • 只有當 type 值為 4 時,valuestring 欄位才有效
    • 只有當 type 值為 3 時,valueint 或 valuedouble 欄位才有效

由於我在實際使用過程中未涉及 bool、null,所以舉例中暫不涉及這兩種型別。

3.3 反序列化 JSON 字串

在正式講解之前,讓我們先看一下與反序列化相關的函數:

函數 解釋說明 返回值
cJSON_Parse 將 JSON 字串反序列化為 cJSON 結構體 cJSON *
cJSON_GetObjectItem 獲取 JSON 物件中的指定項 cJSON *
cJSON_GetArrayItem 獲取 JSON 陣列中的第 i 個 JSON 項 cJSON *
cJSON_GetArraySize 獲取 JSON 陣列的大小(該陣列中包含的 JSON 項個數) int
cJSON_Delete 刪除 cJSON 結構體 void

3.3.1 一個簡單的例子

對於一個 JSON 字串:

{
    "name": "張三",
    "age": 18,
    "sex": "男"
}

我們可以在程式碼中通過呼叫cJSON_Parse(const char *value)函數將 JSON 字串 value 反序列化為 cJSON 結構體:

cJSON *root = cJSON_Parse(pcJson); // pcJson 為從檔案中獲取的 JSON 字串
if (NULL == root)
{
    printf("fail to call cJSON_Parse\n");
    exit(0);
}

反序列化後的 JSON 字串,大概長這個樣子:

  • 圖中的灰色虛線是假想的,實際是不存在的
  • 用來表明 name、age、sex 都是 root 的 child,只不過實際的 child 僅僅指向了第一個節點,也就是 name

那麼我們如何獲取 name、age、sex 對應的值呢?可以通過呼叫cJSON *cJSON_GetObjectItem()函數從 object 中獲取。

cJSON *pName = cJSON_GetObjectItem(root, "name");
printf("name [%s]\n", pName->valuestring);

cJSON *pAge = cJSON_GetObjectItem(root, "age");
printf("age  [%d]\n", pAge->valueint);

cJSON *pSex = cJSON_GetObjectItem(root, "sex");
printf("sex  [%s]\n", pSex->valuestring);
  • cJSON *cJSON_GetObjectItem(cJSON *object, const char *string):從 object 的所有 child 中檢索鍵為 string 的 JSON 項
    • 如果找到則返回相應的 JSON 項
    • 反之返回 NULL。

完整程式碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
#include "cJSON.h"

#define STRING_LEN_MAX 2048

void GetJSONFromFile(const char *FILENAME, char **ppcJson)
{
    FILE *fp = fopen(FILENAME, "r");
    if (NULL == fp)
    {
        printf("file open error\n");
        exit(0);
    }

    char *pcJson = (char *)malloc(STRING_LEN_MAX);
    memset(pcJson, 0, STRING_LEN_MAX);

    do
    {
        fgets(pcJson + strlen(pcJson), STRING_LEN_MAX - strlen(pcJson), fp);
    } while (!feof(fp));

    *ppcJson = pcJson;

    fclose(fp);
}

int main()
{
    char *pcJson;

    GetJSONFromFile("test.json", &pcJson); // 從檔案 test.json 中獲取 JSON 字串

    cJSON *root = cJSON_Parse(pcJson);
    if (NULL == root)
    {
        printf("fail to call cJSON_Parse\n");
        exit(0);
    }

    cJSON *pName = cJSON_GetObjectItem(root, "name");
    printf("name [%s]\n", pName->valuestring);

    cJSON *pAge = cJSON_GetObjectItem(root, "age");
    printf("age  [%d]\n", pAge->valueint);

    cJSON *pSex = cJSON_GetObjectItem(root, "sex");
    printf("sex  [%s]\n", pSex->valuestring);

    cJSON_Delete(root);	// 手動呼叫 cJSON_Delete 進行記憶體回收
    
    return 0;
}

3.3.2 一個有一丟丟複雜的例子

對於一個複雜些的 JSON 字串:

{
    "class_name": "計科一班",
    "stu_num"   : 2,
    "stu_info"  : 
    [
        {
            "name": "張三",
            "age": 18,
            "sex": "男"
        },
        {
            "name": "李四",
            "age": 20,
            "sex": "男"
        }
    ]
}

反序列化該字串依舊很簡單,只需我們在程式碼中呼叫cJSON_Parse()即可,而難點在於如何解析。

先來看一下該字串反序列化後長啥樣:

對於 class_name 以及 stu_name,我們可以很容易就解析出來:

cJSON *pClassName = cJSON_GetObjectItem(root, "class_name");
printf("class name [%s]\n", pClassName->valuestring);

cJSON *pStuNum = cJSON_GetObjectItem(root, "stu_num");
printf("stu num    [%d]\n", pStuNum->valueint);

那麼如何獲取更深層次的 name、age 以及 sex 呢?

通過 JSON 字串可以知道,stu_info 是一個 JSON 陣列,那麼我們首先要做的是將這個陣列從 root 中摘出來:

cJSON *pArray = cJSON_GetObjectItem(root, "stu_info");
if (NULL == pArray)
{
    printf("not find stu_info\n");
    goto err;
}

接著將該陣列中的各個項依次取出。

cJSON *item_0 = cJSON_GetArrayItem(pArray, 0);
cJSON *item_1 = cJSON_GetArrayItem(pArray, 1);
  • cJSON_GetArrayItem(cJSON *array, int item):從 JSON 陣列 array 中獲取第 item 項(下標從 0 開始)

    • 如果存在,則返回相應的 JSON 項

    • 反之返回 NULL。

最後,將 name、age、sex 分別從 item_0 / item_1 中取出即可。

上述操作只是為了講解如何獲取更深層次的 JSON 項,實際操作中會這麼寫:

int iArraySize = cJSON_GetArraySize(pArray);
for (i = 0; i < iArraySize; i++)
{
    printf("******** Stu[%d] info ********\n", i + 1);

    cJSON *item = cJSON_GetArrayItem(pArray, i);

    cJSON *pName = cJSON_GetObjectItem(item, "name");
    printf("name  [%s]\n", pName->valuestring);

    cJSON *pAge = cJSON_GetObjectItem(item, "age");
    printf("age   [%d]\n", pAge->valueint);

    cJSON *pSex = cJSON_GetObjectItem(item, "sex");
    printf("sex   [%s]\n", pSex->valuestring);
}

就跟剝洋蔥似的,先將外層的 stu_info 剝出來,然後在剝出內層的 item,最後將 name、age、sex 從 item 中分離出來。

對於更多層次的 JSON 處理,也是同樣的操作,你只需要保證在解析你所需的 JSON 項前,其父節點已被解析。

完整程式碼如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
#include "cJSON.h"

#define STRING_LEN_MAX 2048

void GetJSONFromFile(const char *FILENAME, char **ppcJson)
{
    FILE *fp = fopen(FILENAME, "r");
    if (NULL == fp)
    {
        printf("file open error\n");
        exit(0);
    }

    char *pcJson = (char *)malloc(STRING_LEN_MAX);
    memset(pcJson, 0, STRING_LEN_MAX);

    do
    {
        fgets(pcJson + strlen(pcJson), STRING_LEN_MAX - strlen(pcJson), fp);
    } while (!feof(fp));

    *ppcJson = pcJson;

    fclose(fp);
}

int main()
{
    char *pcJson;

    GetJSONFromFile("test.json", &pcJson);

    cJSON *root = cJSON_Parse(pcJson);
    if (NULL == root)
    {
        printf("fail to call cJSON_Parse\n");
        exit(0);
    }

    cJSON *pClassName = cJSON_GetObjectItem(root, "class_name");
    printf("class name [%s]\n", pClassName->valuestring);

    cJSON *pStuNum = cJSON_GetObjectItem(root, "stu_num");
    printf("stu num    [%d]\n", pStuNum->valueint);

    cJSON *pArray = cJSON_GetObjectItem(root, "stu_info");
    if (NULL == pArray)
    {
        printf("not find stu_info\n");
        goto err;
    }
    int i;
    int iArraySize = cJSON_GetArraySize(pArray);
    for (i = 0; i < iArraySize; i++)
    {
        printf("******** Stu[%d] info ********\n", i + 1);

        cJSON *item = cJSON_GetArrayItem(pArray, i);

        cJSON *pName = cJSON_GetObjectItem(item, "name");
        printf("name  [%s]\n", pName->valuestring);

        cJSON *pAge = cJSON_GetObjectItem(item, "age");
        printf("age   [%d]\n", pAge->valueint);

        cJSON *pSex = cJSON_GetObjectItem(item, "sex");
        printf("sex   [%s]\n", pSex->valuestring);
    }
    
err:
    cJSON_Delete(root); // 手動呼叫 cJSON_Delete 進行記憶體回收

    return 0;
}

3.4 序列化 cJSON 結構體

前面我們一直在介紹如何將一個 JSON 字串反序列化為 cJSON 結構體,下面我們來介紹一下如何將 cJSON 結構體序列化為 JSON 字串。

首先,我們要先有一個 cJSON 結構體,構造 cJSON 結構體的相關函數如下:

函數 解釋說明 返回值
cJSON_CreateObject 建立一個 object 型別的 JSON 項 cJSON *
cJSON_CreateArray 建立一個 array 型別的 JSON 項 cJSON *
cJSON_CreateString 建立一個值為 string 型別的 JSON 項 cJSON *
cJSON_CreateNumber 建立一個值為 number 型別的 JSON 項 cJSON *
cJSON_AddItemToObject 將 JSON 項新增到 object 中 void
cJSON_AddItemToArray 將 JSON 項新增到 array 中 void
cJSON_AddNumberToObject 建立一個值為 number 型別的 JSON 項並新增到 JSON 物件中 void
cJSON_AddStringToObject 建立一個值為 string 型別的 JSON 項並新增到 JSON 物件中 void
cJSON_Print 將 cJSON 結構體序列化為 JSON 字串(有格式) char *
cJSON_PrintUnformatted 將 cJSON 結構體序列化為 JSON 字串(無格式) char *
cJSON_Delete 刪除 cJSON 結構體 void

3.4.1 一個簡單的例子

假設我們想要獲取的 JSON 字串為:

{
    "name": "張三",
    "age": 18,
    "sex": "男"
}

我們該如何構造 cJSON 結構體呢?

還記得這個 JSON 字串反序列化的樣子嗎?不記得也沒關係,因為我馬上就要張貼了