二. JSON基础数据结构
二. JSON基础数据结构
专栏目录
0. 引
JSON是一个有着特殊结构的数据, 为了解析JSON, 需要使用编程语言将JSON的数据格式进行抽象, 有助于更好地, 快捷地实现JSON数据的解析.
为了使解析JSON结构的性能更好, 选用C语言实现JSON的数据结构的抽象, 以及底层的结构的解析功能实现.
1. JSON基础数据结构
JSON数据的类型只能为JSON标准所规定的五种类型:( JSON的基本数据类型)
-
2. JSON的基本数据类型
- 数值: 十进制数, 不能有前导0, 可以为负数, 可以有小数部分. 还可以用
e
或者E
表示指数部分. 不能包含非数, 如NaN. 不区分整数与浮点数. - 字符串: 以双引号
" "
括起来的零个或者多个
, 支持以反斜杠开始的转义字符序列. - 布尔值: 表示为
true
或者false
- 数组: 有序的零个或者多个值. 每个值可以为任意类型. 数组使用方括号包裹. 多个数组元素之间用逗号分隔.
- 对象: 若干无序的"键-值对", 其中键只能是字符串. 建议但不强制要求对象中的键是独一无二的. 对象以花括号
{}
包裹. 多个键-值对之间使用逗号,
分隔. 键与值之间使用冒号:
分隔. - 空值: 值写为
null
- 数值: 十进制数, 不能有前导0, 可以为负数, 可以有小数部分. 还可以用
1.1 JSON数据的抽象(json_value_t)
从最开始, 我们需要一个数据结构, 这个结构可以表示所有合法类型的JSON数据.
这个结构应该有以下成员:
- 数据的类型(type), 用于表示当前数据的类型, 由于json数据支持的类型只有6种, 所以这个成员使用int便可以存储
- 数据的值(value), 类型暂未知, 暂用
TYPE
表示.
-
根据以上的简单分析, 可以得到以下代码:
typedef struct __json_value json_value_t struct __json_value { int type; TYPE value; };
对于type, 所有的类型可能都已知, 其中
bool
类型的true
和false
可作为单独的类型表示, 即总共存在7种类型, 可使用C语言int进行表示.对于value, 最直觉的方式是可以使用一个结构体进行存储, 结构体中包含type对应的value, 而每一个特定的
json
数据, 其type和value已经固定, 故可以使用union
进行表示.更近一步的, 对于
null
类型和bool
类型中的true
和false
类型, 由于这三种类型的所对应的值只有一种固定的表示, 故可以直接使用type来表示其值.现在需要表示的value剩下:
number
,string
,array
,object
其中,
number
和string
可直接使用C语言内置类型double
和char *
进行表示,array
和object
作为复合类型, 使用另一个自定义的结构体进行定义, 该结构体命名上为了和__json_value
保持一致, 使用json_array_t
和json_object_t
表示, 其具体实现, 后面展开.
-
综合上面的分析, 可以得到json数据的抽象结构如下
#define JSON_VALUE_STRING 1 #define JSON_VALUE_NUMBER 2 #define JSON_VALUE_OBJECT 3 #define JSON_VALUE_ARRAY 4 #define JSON_VALUE_TRUE 5 #define JSON_VALUE_FALSE 6 #define JSON_VALUE_NULL 7 typedef struct __json_value json_value_t; struct __json_value { int type; union { char *string; double number; json_object_t object; json_array_t array; } value; };
1.2 JSON数组结构抽象(json_array_t)
数组: 有序的零个或者多个值. 每个值可以为任意类型. 数组使用方括号包裹. 多个数组元素之间用逗号分隔.
从JSON标准对数组的定义, 可以得知, JSON数组本质上是一个有序的序列, 其他的描述, 如方括号包裹, 仅作为解析的标准, 不影响数据抽象.
为了存储一个有序的序列, 比较容易想到的方式就是数组或者链表, 下面简单分析一下数组和链表的优缺点.
-
数组在内存中是连续分配的, 随机访问能力强(即访问第1个元素和访问第1000个元素的效率相同), 但也正因为数组是连续分配的, 所以数组扩容需要重新分配内存, 同时新增和修改元素的效率低.
-
链表在内存中是不连续的, 新增和修改的效率高, 但随机访问能力低(访问都需要从头指针开始遍历).
在JSON的array中, 随机访问的场景较少, 在解析中, 需要频繁插入元素, 同时插入的元素数量未知, 故在这里, 选用链表结构存储JSON数组会更好一些.
简单说一下链表的结构, 链表是由零个或多个节点组成的线性表. 每一个节点中大多包含一个数据域和一个指针域, 数据域存储当前节点的数据, 指针域指向下一个节点(双向链表指针域有两个, 一个指向前一个节点, 另一个指向后一个节点)
-
根据以上分析, 可以得到以下简单代码:
typedef struct __json_array json_array_t; typedef struct __json_element json_element_t; struct __json_array { int size; json_element_t* head; }; struct __json_element { json_value_t value; json_element_t* next, * prev; };
上面代码是一个很简单的链表结构,
__json_array
结构包含两个值, 一个表示数组的大小, 另一个是一个类型为json_element_t
的指针, 将**json_element_t**
看做一个链表, 那么这个指针就是指向这个链表的头结点.将JSON的数组抽象为一个链表结构, 数组中的每个元素就可以抽象为链表里的一个个节点, 这样, JSON数组就可以通过链表进行抽象表示了.
但仅仅是这样, 还是不能直接使用, 因为我们没有实现链表相关的操作, 需要自行实现链表的插入, 删除等操作.
实现链表的相关操作, 并不复杂, 在Barenboim/json-parser项目中, 使用的是Linux内核实现的链表, 为了致敬Linux内核源码, 和保持和Barenboim/json-parser的一致性, 所以也采用Linux内核源码(list.h)中所实现的链表结构.
Linux内核链表(list.h)
-
这里简单说一下Linux内核实现的链表(list.h)
struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) \ { \ &(name), &(name) \ } #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) static inline void INIT_LIST_HEAD(struct list_head* list) { list->next = list; list->prev = list; }
-
可以看到
list_head
结构只有指针域, 而没有数据域, 这样设计是为了通用性, 想要真正使用这个链表, 需要将这个链表放在另一个自建的结构中组合使用.struct list_head { struct list_head *next, *prev; }; struct __json_element { struct list_head list; json_value_t value; };
-
经过改造, 可以看到, 我们将原本的
__json_element
中的指针域换成内核链表struct __json_element
, 这样改造就完成了. -
改造成这个样子, 最大的问题是如何通过list指针获取到value, 原本的指针类型就是
__json_element
, 可以直接通过->
操作符获取到value, 但是现在的难以获取到value的值. -
为了通过list指针获取到其宿主结构的其他成员,
list.h
提供了一个list_entry
宏, 通过这个神奇的宏, 可以获取到list指针的宿主结构的指针./** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_struct within the struct. */ #define list_entry(ptr, type, member) \ ((type*)((char*)(ptr) - (size_t)(&((type*)0)->member))) struct __json_element { struct list_head list; json_value_t value; }; struct __json_element ele; struct __json_element eleNext = list_entry(&ele.list.next, struct __json_element, list);
- 上面代码中举了一个简单的例子, 声明一个元素
ele
, 那么通过list_entry
可以获取到指向ele
的下一个元素的指针. - 首先是拥有list的地址(即ptr)将其强转为char*是为了将这个指针的加减步长操作转为一字节, 这是减号的前半部分.
- 减号后半部分, 设置一个
type
类型的零指针, 在这里type为struct __json_element
, 获取这个0指针中的member
成员的地址, 由于这个结构体的地址为0, 那么通过0指针获取到的结构体中成员的地址, 就是相对于0的地址, 也就是这个成员在结构体中的地址偏移量. - 现在已知
list
的地址, 又已知**list**
在其宿主结构体中的地址偏移量, 两个相减, 就得到了宿主结构体的地址, 最后将其转为指定的type
就行了.
- 上面代码中举了一个简单的例子, 声明一个元素
-
list.h中最关键的就是通过
list_entry
获取宿主结构体的指针, list.h中还包含其他链表相关的常规操作, 如添加节点, 删除节点等, 这里就不讲了.
-
经过链表的改造, 可以得到json数组的结构体如下:
typedef struct __json_array json_array_t; typedef struct __json_element json_element_t; struct __json_array { int size; // json数组的头指针 struct list_head head; }; struct __json_element { struct list_head list; json_value_t value; };
1.3 JSON对象数据结构抽象
对象: 若干无序的"键-值对", 其中键只能是字符串. 建议但不强制要求对象中的键是独一无二的. 对象以花括号{}
包裹. 多个键-值对之间使用逗号,
分隔. 键与值之间使用冒号:
分隔.
根据上面JSON对象的标准定义, 我们可以知道, JSON对象类型, 是一个无序的序列, 序列中的每个成员是一个键值对.
JSON对象的抽象和JSON数组类似, 两者都是一个序列, 大体上都可以使用链表进行表示, 数组中是一个个的元素, 而对象中是一个个的键值对, 将一个个键值对称为对象的成员.
综合上面的分析, 可以初步得到如下代码:
typedef struct __json_object json_object_t;
struct __json_object
{
int size;
struct list_head head;
};
struct __json_member
{
struct list_head list;
char key[1];
json_value_t value;
};
-
__json_object
结构中包含对象的size, 和指向对象的第一个成员的指针, 和数组结构一样, 其中包含一个链表结构. -
__json_member
结构中包含链表指针, 和key, value, 很好理解
我们常说JSON, 常常是以一个JSON对象来说的, 经常遇到的一个场景就是需要去查找JSON对象中的一个键值对, 获取对应键中的值, 所以在JSON对象中做查找, 是很常用的一个功能.
众所周知, 链表查找的时间复杂度为O(n), 这在频繁查找的场景下, 效率是不够好的.
而说到查找, 常见的便于查找的数据结构就是二叉查找树, 如果我们在解析JSON结构的时候, JSON对象中的每个成员放入二叉查找树中, 那么未来查找就会比较方便了, 直接选用红黑树来助力实现JSON对象的查找.
我们可以直接引用linux内核的红黑树实现, 使用和链表的方式类似, 直接在我们原有的结构体中添加红黑树的指针就行了.
struct __json_object
{
int size;
struct list_head head;
struct rb_root root;
};
struct __json_member
{
struct list_head list;
struct rb_node node;
char key[1];
json_value_t value;
};
在
__json_object
结构中添加了一个struct rb_root
结构, 这个表示红黑树的根节点,__json_member
结构中添加了红黑树的子节点结构rb_node
rbtree中包含了红黑树相关的操作函数, 这里不过多深究, 直接用就行了.
2. 总结
经过上面的分析, 我们已经把JSON所有的类型都进行了抽象, 总体的JSON的数据使用__json_value
进行表示, 不同的类型的值的表示方法总结如下
- 数值(number): 直接使用
double
- 字符串(string): 直接使用
char *
- 布尔值(bool): 直接使用结构体中的type(int)的指定值进行表示
- 数组(array): 自定义一个
__json_array_t
结构进行表示(存储结构为链表) - 对象(object): 自定义一个
__json_object_t
结构进行表示(存储结构为链表 + 红黑树)
#include "list.h"
#include "rbtree.h"
#define JSON_VALUE_STRING 1
#define JSON_VALUE_NUMBER 2
#define JSON_VALUE_OBJECT 3
#define JSON_VALUE_ARRAY 4
#define JSON_VALUE_TRUE 5
#define JSON_VALUE_FALSE 6
#define JSON_VALUE_NULL 7
typedef struct __json_value json_value_t;
typedef struct __json_object json_object_t;
typedef struct __json_array json_array_t;
typedef struct __json_member json_member_t;
typedef struct __json_element json_element_t;
struct __json_value
{
int type;
union
{
char *string;
double number;
json_object_t object;
json_array_t array;
} value;
};
struct __json_array
{
int size;
struct list_head head;
};
struct __json_element
{
struct list_head list;
json_value_t value;
};
struct __json_object
{
int size;
struct list_head head;
struct rb_root root;
};
struct __json_member
{
struct list_head list;
struct rb_node node;
char key[1];
json_value_t value;
};
至此, 我们已经实现了JSON结构中的所有类型的C语言抽象, 接下来就是JSON数据的解析了, 如果对JSON结构有一定的了解, 解析不过是操作这个相应的数据结构罢了.
参考
[1] Workflow 源码解析 Json parser :part1 parse - 知乎 (zhihu.com)
[2] 维基百科(JSON)