要做存储业务,我解析了一个项目的源码
最近在做存储相关的业务,更具体的来说是存储相关的研发,于是就上网查了一下相关的资料,思虑再三打算从最简单的 Json 数据交换格式开始研究。
JSON是独立于编程语言的数据交换格式,几乎所有与网络开发相关的语言都有JSON函数库,例如:C语言里的cJSON,Golang语言里的package json,Java语言里的jackson,Python语言中的pyson等。
1、JSON介绍
2、cJSON源码分析
3、如何设计“key-value”的数据结构
4、如何设计JSON节点操作函数
5、如何设计解析JSON字符串函数
6、如何设计JSON节点转JSON字符串
1、JSON介绍
可能有些还是比较还在读书的学弟学妹还不知道什么是JSON,有什么约定,这里就大致介绍一下,已经了解的可以直接跳过该部分。
要开发JSON编解码器,当然要了解一下什么是JSON
JSON基本数据格式采用的是“键值对”,即“key : value”形式,其中key只能是字符串,而value的数据类型有多种;多个“键值对”之间使用逗号,
分隔,键与值之间用冒号:
分隔
JSON的value基本数据类型:
- 数值(number):十进制数,不能有前导0,可以为负数,可以有小数部分。不区分整数与浮点数
- 字符串(string):以双引号
""
括起来的零个或多个Unicode码位。支持反斜杠开始的转义字符序列 - 对象(object):由无序的“键值对”组成,使用花括号
{}
包裹 - 数组(array):有序的零个或者多个值,每个值可以为任意类型,使用方括号包裹,形如:
[value, value]
- true:布尔值true
- false:布尔值false
- null:空值
四个特定字符被认为是空白符:空格符(' ')、水平制表符('\t')、回车符('\r')、换行符('\n')
JSON字符串中,在元素前后允许存在无意义的空白符(会被忽略)
JSON字符串示例
{
"firstName": "John",
"lastName": "Smith",
"isAlive": true,
"age": 27,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-3100"
},
"children": [
"Catherine",
"Thomas",
"Trevor"
],
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567"
}
],
"spouse": null
}
2、cJSON源码分析
这里选择cJSON做进一步了解和解析,因为C语言是编程语言之母,也是我的第一门语言。
cJSON是github上一个star数为8.8k的仓库,是很多C/C++从业者开始学习源码知识都会开始选择的一个项目。
本文将对cJSON源码进行一个介绍,碍于篇幅原因写不了太多东西,这里只写一些源码解析过程中的宏观介绍。即使是这样,这篇文章也有将近5900 字之多。
所以具体的实现细节可以参考附详细注释的源码仓库:https://github.com/MingWangSong/MycJSON
我们了解清楚JSON数据的约定后,即可边提设计需求边分析cJSON是如何实现,
JSON编解码工具包主要提供JSON序列化和反序列化的这个两大块功能。
其中,序列化是指将编程语言结构化数据转化为JSON字符串;反序列化则是指相反的操作。
表述形象一点,JSON序列化就是输出或者打印JSON字符串,JSON反序列化操作是为了解析JSON字符串。
JSON数据整体就是一个对象类型的数据,可以假象成一个key缺省的根节点;将一个“key-value”视为JSON中的基本结构,整个JSON中处处为“key-value”(这句式有Linux味)。
因此,要想实现JSON序列化和反序列化,则需定义“key-value”的数据结构,该结构称之为JSON节点。
3、如何设计“key-value”的数据结构
- “key-value”数据结构的设计需要考虑两个事情:单一“key-value”的数据表示和多个“key-value”之间的关系表示
- 针对多个“key-value”之间的关系表示,cJSON的设计想法是通过链表来串联JSON节点,具体来讲,使用单链表实现JSON嵌套关系,使用双向链表实现JSON同级关系。
因此,后续的所有JSON节点操作都很链表相关。下面举例绘图说明:
{
"firstName": "John",
"lastName": "Smith",
"children": [
"Catherine",
"Thomas",
"Trevor"
],
"address": {
"streetAddress": "2nd-Street",
"city": "New York"
}
}
cJSON处理上述JSON数据的结构示意图如下:
- 在上图第一列中,黄色块为JSON根节点,该节点key缺省,value对应JSON字符串最外围
{}
包裹的若干个JSON节点,这些节点为根节点的子节点,图中红色箭头代表指向子节点的指针;
第二列代表根节点下的一级JSON子节点,同级节点构成双链表,图中黑色指针为双链表的前驱和后继指针。
值得注意的是,cJSON处理数组对象时,是将数组中每个元素视为一个JSON节点存储,同样采用双链表的形式存储,数组类型的JSON节点的value是没有值的,而是通过指向子节点的指针来存储数组地址,因此对象类型节点和数组类型节点的后续很多操作都可复用,非常的妙! - cJSON节点数据结构设计如下:
typedef struct cJSON {
struct cJSON *next; // 同级节点后继指针
struct cJSON *prev; // 同级节点前驱指针
struct cJSON *child; // 子节点指针
int type; // “value”的类型,一共有9种类型
char *valuestring; // “value”类型为字符串对应的存值变量
int valueint; // “value”类型为整数对应的存值变量(写入valueint已弃用)
double valuedouble; // “value”类型为浮点数对应的存值变量
char *string; // 键值对中的“key”值
} cJSON;
其中,next,prev,child
分别代表前驱节点、后继节点和子节点,用于处理JSON数据节点间的层级关系;type表示该JSON节点的value类型。特别注意,变量string代表着key(
这个命名让我很不爽)。
因为类型定义中区分了布尔值的true和false两种类型,所以布尔类型的JSON节点实际不需要其他变量存储value值。具体类型定义如下:
#define cJSON_Invalid (0) // 非法类型
#define
#define
#define
#define
#define
#define
#define
#define cJSON_Raw (1 << 7) // raw json
#define
#define
4、如何设计JSON节点操作函数
无论是JSON序列化还是反序列化,都无法避免针对JSON节点的操作,因此先了解cJSON中JSON节点操作设计
- 创建JSON节点
- 上图为cJSON中创建JSON节点的调用流程图。根据cJSON源码实现方式,创建JSON结点函数可大致分为三类:创建非数组类型JSON节点,创建数组类型JSON节点,创建JSON节点引用。由于同类函数实现逻辑相似。
创建JSON节点拢共分三步:开辟空间,设置节点类型,设置节点value。
注意,创建节点的时候不会设置key值,因此要新增节点的时候不会直接调用此类函数,而是提供了对应的Add函数,后文将会详细介绍。
- 创建非数组类型JSON节点
创建非数组类型节点时,先调用cJSON_New_Item()
为cJSON节点申请空间并用memset进行初始化,然后再设置节点类型和value。如果是添加指定的节点到指定的对象或者数组,则需设置next
或child
指针,源码如下:
cJSON * cJSON_CreateObjectReference(const cJSON *child){
cJSON *item = cJSON_New_Item(&global_hooks);
if (item != NULL) {
item->type = cJSON_Object | cJSON_IsReference;
item->child = (cJSON*)cast_away_const(child);
}
return item;
}
cJSON * cJSON_CreateArrayReference(const cJSON *child){
cJSON *item = cJSON_New_Item(&global_hooks);
if (item != NULL) {
item->type = cJSON_Array | cJSON_IsReference;
item->child = (cJSON*)cast_away_const(child);
}
return item;
}
- 创建数组类型JSON节点
创建数组类型节点时,先调用cJSON_New_Item()
为cJSON节点申请空间并用memset进行初始化,然后再设置节点类型,接着,需要根据数组数组构建子节点链表,子节点链表是双向链表,并且头结点前驱指针指向尾结点,以便尾插入的时候快速定位尾结点。
2.新增JSON节点
由于cJSON中采用链表结构存储节点,因此新增JSON节点涉及的是链表中的插入操作。因为在cJSON中,value中的数组元素也是采用JSON节点来存储的,所以在数组用添加元素也是一样的链表操作。
由上图可知,新增JSON节点的核心函数是add_item_to_array()
,该方法已尾插法的形式插入新建的JSON节点,为了能快速找到尾结点,每次插入时会更新头结点的前驱指针指向尾结点,具体插入过程如下图所示。
cJSON还提供了cJSON_InsertItemInArray()
,该函数实现了在数组任意位置插入元素。
3.替换JSON节点
替换JSON节点的实现核心函数是cJSON_ReplaceItemViaPointer
,主要是双链表中元素的替换操作。
4.删除JSON节点
删除JSON节点的核心函数是cJSON_Delete
和cJSON_DetachItemViaPointer
,递归删除指定节点及其所有子节点,但是cJSON_IsReference类型的节点不会删除。
其中,cJSON_DetachItemViaPointer
主要处理待删除节点的相关指针;cJSON_Delete
主要用于释放删除后的节点存储空间。
5.查找JSON节点
查找JSON节点的核心函数是get_object_item
,由于是链表存储结构,因此只能遍历查找。
5、如何设计解析JSON字符串函数
使用cJSON来解析JSON节点时,直接调用cJSON_Parse()
函数,上图展示该函数的调用关系,核心函数是parse_value
和parse_object
。其中,parse_value
实现了分别解析不同的类型的value值,布尔类型和null
类型直接在本函数中处理,数字、字符串、数组、对象类型通过封装函数分别处理;parse_object
主要实现了针对JSON节点的解析,通过先借助parse_string
解析key值,再通过parse_value
解析value值。
特别地,因为cJSON中嵌套对象和数组的数据结构设计是一样的,因此parse_object
和parse_array
的实现几乎一样,区别主要在于parse_array
没有key值需要解析。
6、如何设计JSON节点转JSON字符串函数
对比解析JSON字符串函数的函数调用图可以发现,JSON节点转JSON字符串函数的设计逻辑类似,也包含两个核心函数print_value
和print_object
,这里就不做更进一步的解析了。
希望今天的源码解析能对大家有帮助,以后会带来更多的源码级项目解析。
参考链接
- DaveGamble/cJSON: Ultralightweight JSON parser in ANSI C (github.com):https://github.com/DaveGamble/cJSON
- JSON - 维基百科,自由的百科全书 (wikipedia.org):https://zh.wikipedia.org/zh-cn/JSON
- JSON:https://www.json.org/json-zh.html
- cJSON源码及解析流程详解_Tyler_Zx的博客-CSDN博客:https://blog.csdn.net/qq_38289815/article/details/103307262
- 如何解析JSON数据及内存钩子的使用方法-腾讯云开发者社区-腾讯云 (tencent.com):https://cloud.tencent.com/developer/beta/article/1662821
- cJSON更换默认的malloc函数 - chilkings - 博客园 (cnblogs.com):https://www.cnblogs.com/chilkings/p/15821140.html
- cjson库的使用以及源码阅读 - cfzhang - 博客园 (cnblogs.com):https://www.cnblogs.com/cfzhang/p/99da02ab2f02520d458c415a5314f83d.html
- C语言中.h和.c文件解析(很精彩)-腾讯云开发者社区-腾讯云 (tencent.com):https://cloud.tencent.com/developer/article/1690858
- MingWangSong/MycJSON (github.com):https://github.com/MingWangSong/MycJSON
文章转载自公众号:拓跋阿秀