2

cJson 学习笔记 - MElephant

 1 year ago
source link: https://www.cnblogs.com/hyacinthLJP/p/16908345.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

cJson 学习笔记

思考这么一个问题:对于不同的设备如何进行数据交换?可以考虑使用轻量级别的 JSON 格式。

那么需要我们手写一个 JSON 解析器吗?这大可不必,因为已经有前辈提供了开源的轻量级的 JSON 解析器——cJSON。我们会用就可以了,当然你也可以深入源码进行学习。

下图则向我们展示了如何通过 cJSON 实现 Client 与 Server 的数据交换:

1494888-20221120134218163-703341249.png
  • 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 对象具体格式如下图所示:

1494888-20221120134217807-1472168311.png
  • 一个对象以{开始,以}结束,是若干「key / value 对」的集合
  • key 和 value 使用:分隔
  • key / value 对之间使用,分隔

注意事项:

  1. 键:必须是 string 类型
  2. 值:可以是合法的 JSON 数据类型(字符串、数值、对象、数组、布尔值或 null)

如,这是一个合法的 JSON 对象:

{
    "name" : "张三"
}

这也是一个合法的 JSON 对象:

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

1.4 JSON 数组

JSON 数组具体格式如下图所示:

1494888-20221120134217523-1849030372.png
  • 一个数组以[开始,]结束,是若干 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,这些取值是可以嵌套的。

1494888-20221120134217234-796635162.png

在「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(如下图所示)警告:

1494888-20221120134216933-975813848.png

不要慌,这并不影响程序的运行,如果你想消除这些警告,不妨将 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 字符串,大概长这个样子:

1494888-20221120134216581-1550828662.png
  • 图中的灰色虚线是假想的,实际是不存在的
  • 用来表明 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()即可,而难点在于如何解析。

先来看一下该字符串反序列化后长啥样:

1494888-20221120134216270-1499947684.png

对于 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 字符串反序列化的样子吗?不记得也没关系,因为我马上就要张贴了😝

1494888-20221120134215935-737988475.png

根据图示可知,我们要先有一个根节点 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": "男"
        }
    ]
}

对应的反序列化后的模样如下图所示:

1494888-20221120134215478-1510756546.png

具体做法为:

  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;
}

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK