cJson 學習筆記
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 的兩種數據結構:
- 對象:A collection of key/value pairs(一個無序的 key / value 對的集合)
- 數組:An ordered list of values(一 / 多個 value 的有序列表)
從上述描述中,我們可以獲得如下四種資訊:
- 對象(Object)
- 數組(Array)
- 鍵(key)
- 值(Value)
1.3 JSON 對象
JSON 對象具體格式如下圖所示:
- 一個對象以
{
開始,以}
結束,是若干「key / value 對」的集合 - key 和 value 使用
:
分隔 - key / value 對之間使用
,
分隔
注意事項:
- 鍵:必須是 string 類型
- 值:可以是合法的 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 使用說明
源碼下載://www.aliyundrive.com/s/vms4mGLStGm
編譯環境:CentOS 7
源碼中包含 cJSON.h 和 cJSON.c,以及一個測試程式 main.c,測試程式的功能是輸出一個 JSON 字元串:
{
"name": "張三",
"age": 18,
"sex": "男"
}
你可以通過下面兩種方法運行該程式:
$ gcc *.c -o main -lm
,會生成一個可執行文件 main,執行該文件即可- 將 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 字元串反序列化的樣子嗎?不記得也沒關係,因為我馬上就要張貼了😝
根據圖示可知,我們要先有一個根節點 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": "男"
}
]
}
對應的反序列化後的模樣如下圖所示:
具體做法為:
- 首先創建一個 root
- 將第二層的 class_name、stu_num、stu_info 加入到 root 中
- 構造兩個 JSON 項 item_0 和 item_1,並將 name、age、sex 分別加入其中
- 最後將 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;
}
參考資料
- cJSON 使用筆記 – 魚竿的傳說 – 部落格園 (cnblogs.com)
- 使用 CJSON 在C語言中進行 JSON 的創建和解析的實例講解 – fengbohello – 部落格園 (cnblogs.com)
- cJSON學習筆記_xukai871105的部落格-CSDN部落格
- 認識Json本質 & 一個較複雜Json串的解析實例 – 灰信網(軟體開發部落格聚合) (freesion.com)
- 零基礎學習cJSON 源碼詳解與應用(一)如何學習cJSON_killer-p的部落格-CSDN部落格