思考這麼一個問題:對於不同的裝置如何進行資料交換?可以考慮使用輕量級別的 JSON 格式。
那麼需要我們手寫一個 JSON 解析器嗎?這大可不必,因為已經有前輩提供了開源的輕量級的 JSON 解析器——cJSON。我們會用就可以了,當然你也可以深入原始碼進行學習。
下圖則向我們展示瞭如何通過 cJSON 實現 Client 與 Server 的資料交換:
在介紹 cJSON 之前,先來對 JSON 這個資料格式有個簡單瞭解。
JSON 指的是 JavaScript 物件表示法(JavaScript Object Notation)。但它並不是程式語言,而是一種可以在伺服器和使用者端之間傳輸的資料交換格式。
JSON 的兩種資料結構:
從上述描述中,我們可以獲得如下四種資訊:
JSON 物件具體格式如下圖所示:
{
開始,以}
結束,是若干「key / value 對」的集合:
分隔,
分隔注意事項:
如,這是一個合法的 JSON 物件:
{
"name" : "張三"
}
這也是一個合法的 JSON 物件:
{
"name" : "張三",
"age" : 18,
"sex" : "男"
}
JSON 陣列具體格式如下圖所示:
[
開始,]
結束,是若干 value 的有序集合,
分隔如,這是一個合法的 JSON 陣列:
[
"張三",
18,
"男"
]
該陣列包含三個 value,分別為 string、number、string
這也是一個合法的 JSON 陣列:
[
{
"name" : "張三",
"age" : 18,
"sex" : "男"
},
{
"name" : "李四",
"age" : 19,
"sex" : "男"
}
]
該陣列包含兩個 Object,每個 Object 又包含若干 key / value。
值(value)可以是:
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" : "男"
}
]
}
原始碼下載:https://www.aliyundrive.com/s/vms4mGLStGm
編譯環境:CentOS 7
原始碼中包含 cJSON.h 和 cJSON.c,以及一個測試程式 main.c,測試程式的功能是輸出一個 JSON 字串:
{
"name": "張三",
"age": 18,
"sex": "男"
}
你可以通過下面兩種方法執行該程式:
$ gcc *.c -o main -lm
,會生成一個可執行檔案 main,執行該檔案即可由於原始碼中使用了 pow、floor 函數,所以在編譯的時要連結上 math 庫,也就是 -lm 指令。
如果在編譯過程中報好多 warning(如下圖所示)警告:
不要慌,這並不影響程式的執行,如果你想消除這些警告,不妨將 cJSON.c 格式化一下(用 VSCode 按下alt+shift+F)。
至於原理,不妨參考這篇文章:gcc編譯警告關於(warning: XXX...[-Wmisleading-indentation] if(err)之類的問題)
首先,我們先對 cJSON 的結構體有個初步瞭解,其定義如下:
typedef struct cJSON
{
struct cJSON *next, *prev;
struct cJSON *child;
int type;
char *valuestring;
int valueint;
double valuedouble;
char *string;
} cJSON;
由於我在實際使用過程中未涉及 bool、null,所以舉例中暫不涉及這兩種型別。
在正式講解之前,讓我們先看一下與反序列化相關的函數:
函數 | 解釋說明 | 返回值 |
---|---|---|
cJSON_Parse | 將 JSON 字串反序列化為 cJSON 結構體 | cJSON * |
cJSON_GetObjectItem | 獲取 JSON 物件中的指定項 | cJSON * |
cJSON_GetArrayItem | 獲取 JSON 陣列中的第 i 個 JSON 項 | cJSON * |
cJSON_GetArraySize | 獲取 JSON 陣列的大小(該陣列中包含的 JSON 項個數) | int |
cJSON_Delete | 刪除 cJSON 結構體 | void |
對於一個 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 對應的值呢?可以通過呼叫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 項
完整程式碼如下:
#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;
}
對於一個複雜些的 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;
}
前面我們一直在介紹如何將一個 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 |
假設我們想要獲取的 JSON 字串為:
{
"name": "張三",
"age": 18,
"sex": "男"
}
我們該如何構造 cJSON 結構體呢?
還記得這個 JSON 字串反序列化的樣子嗎?不記得也沒關係,因為我馬上就要張貼了