cJson 學習筆記

cJson 學習筆記

一、前言

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

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

下圖則向我們展示了如何通過 cJSON 實現 Client 與 Server 的數據交換:

image-20221119161207712

  • 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 對象具體格式如下圖所示:

image-20221118232125879

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

注意事項:

  1. 鍵:必須是 string 類型
  2. 值:可以是合法的 JSON 數據類型(字元串、數值、對象、數組、布爾值或 null)

如,這是一個合法的 JSON 對象:

{
    "name" : "張三"
}

這也是一個合法的 JSON 對象:

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

1.4 JSON 數組

JSON 數組具體格式如下圖所示:

image-20221118232514539

  • 一個數組以[開始,]結束,是若干 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,這些取值是可以嵌套的。

image-20221120122601710

在「1.4 JSON 數組」中,第二個例子就是一個嵌套的舉例,當然也可以這麼嵌套:

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

三、cJSON 使用教程

3.1 cJSON 使用說明

源碼下載://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(如下圖所示)警告:

image-20221120120313471

不要慌,這並不影響程式的運行,如果你想消除這些警告,不妨將 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 字元串,大概長這個樣子:

image-20221119221819835

  • 圖中的灰色虛線是假想的,實際是不存在的
  • 用來表明 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()即可,而難點在於如何解析。

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

image-20221119223404283

對於 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 字元串反序列化的樣子嗎?不記得也沒關係,因為我馬上就要張貼了😝

image-20221119225512840

根據圖示可知,我們要先有一個根節點 root。

由於本次樣例中的 JSON 字元串是一個 JSON 對象,所以我們需要通過cJSON_CreateObject()函數來創建一個 object 類型的 root:

cJSON *root = cJSON_CreateObject();

接下來我們需要將 name、age、sex 分別加入到 root 中:

  • 通過cJSON_AddStringToObject()將字元串類型的 name、sex 加入到 root 中
  • 通過cJSON_AddNumberToObject()將數值類型的 age 加入到 root 中

具體操作如下:

cJSON_AddStringToObject(root, "name", "張三");
cJSON_AddNumberToObject(root, "age", 18);
cJSON_AddStringToObject(root, "sex", "男");
  • cJSON_AddStringToObject(object,name,s):將鍵值對(name / s)加入到 object 中
  • cJSON_AddNumberToObject(object,name,n):將鍵值對(name / n)加入到 object 中

經過上述操作,我們就可以得到如圖 6 所示的 cJSON 結構體。那如何獲取基於該結構體的 JSON 字元串呢?

很簡單,調用函數cJSON_Print()cJSON_PrintUnformatted()即可實現:

char *pJsonFormatted = cJSON_Print(root);
puts(pJsonFormatted);

char *pJsonUnformatted = cJSON_PrintUnformatted(root);
puts(pJsonUnformatted);

cJSON_Print()cJSON_PrintUnformatted(),這兩個 API 的區別在於:一個是沒有格式的,也就是轉換出的字元串中間不會有換行、對齊之類的格式存在。而 cJSON_Print 列印出來是我們看起來很舒服的格式,僅此而已。

完整程式碼如下:

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

int main()
{
	cJSON *root = cJSON_CreateObject();
	cJSON_AddStringToObject(root, "name", "張三");
	cJSON_AddNumberToObject(root, "age", 18);
	cJSON_AddStringToObject(root, "sex", "男");

	char *pJsonFormatted = cJSON_Print(root);
	puts(pJsonFormatted);
	
	char *pJsonUnformatted = cJSON_PrintUnformatted(root);
	puts(pJsonUnformatted);

	cJSON_Delete(root); // 手動調用 cJSON_Delete 進行記憶體回收
	
    // 記得回收 pJsonFormatted 和 pJsonUnformatted
	if (NULL != pJsonFormatted)
	{
		free(pJsonFormatted);
	}
	if (NULL != pJsonUnformatted)
	{
		free(pJsonUnformatted);
	}

	return 0;
}

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

這次我們要獲取的 JSON 字元串為:

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

對應的反序列化後的模樣如下圖所示:

image-20221119232056866

具體做法為:

  1. 首先創建一個 root
  2. 將第二層的 class_name、stu_num、stu_info 加入到 root 中
  3. 構造兩個 JSON 項 item_0 和 item_1,並將 name、age、sex 分別加入其中
  4. 最後將 JSON 項 加入到 stu_info 中

大功告成,具體程式碼如下:

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

int main()
{
    // 步驟一
	cJSON *root = cJSON_CreateObject();

    // 步驟二
	cJSON_AddStringToObject(root, "class_name", "計科一班");
	cJSON_AddNumberToObject(root, "stu_num", 2);
	
	cJSON *pArray = cJSON_CreateArray();
	cJSON_AddItemToObject(root, "stu_info", pArray);

   	// 步驟三
	cJSON *pObject_1 = cJSON_CreateObject();
	cJSON_AddStringToObject(pObject_1, "name", "張三");
	cJSON_AddNumberToObject(pObject_1, "age", 18);
	cJSON_AddStringToObject(pObject_1, "sex", "男");
	
	cJSON *pObject_2 = cJSON_CreateObject();
	cJSON_AddStringToObject(pObject_2, "name", "李四");
	cJSON_AddNumberToObject(pObject_2, "age", 19);
	cJSON_AddStringToObject(pObject_2, "sex", "男");
    
    // 步驟四
    cJSON_AddItemToArray(pArray, pObject_1);
	cJSON_AddItemToArray(pArray, pObject_2);

	char *pJson = cJSON_Print(root);
	puts(pJson);


	cJSON_Delete(root); // 手動調用 cJSON_Delete 進行記憶體回收
	if (NULL != pJson)  // 回收 pJson
	{
		free(pJson);
	}
	return 0;
}

參考資料

Tags: