cjson 的源码大约1000行左右,用C语言实现了一个json的解析器。c语言没有字典或key-value这样的数据结构,所以处理json需要自己定义数据结构来处理,想想都是一个很激动的事情。
cjson的官网 : http://www.json.org/
下载源码 : https://sourceforge.net/projects/cjson/
另外,在源码里面可以看到对json字符串的解析过程(其实本质是一个自动机),还有一个问题就是内存的管理,由于c的特性需要自己用户管理内存(即自定义 malloc 和 free)。这几天看完cjson,这个的确是一个很好的教程,来帮助你更好的理解底层,即使你平常不用c写代码。
cjson源码里面试图解决的几个问题
- 定义的数据结构是什么?
- 如何进行内存管理?
2.1 如何创建结点
2.2 如何删除结点 - 如果将一个json类型的字符串转解析成对应的数据结构?
3.1 如何解析字符串
3.2 如何解析数组
3.3 如何解析数字
3.4 如何解析嵌套的json对象 - 对json结点的操作
4.1 添加结点
4.2 删除结点
4.3 查找结点
4.4 修改结点 - 输出和序列化
理解源码,可以按照这个来对照一个,看看作者的思路是如何干的,如果是你,你会怎么做。
1. 定义的数据结构
struct cJSON *next,*prev; /同一级的元素使用双向列表储存/
struct cJSON *child; /* 如果是个 object 或 array 的话,第一个儿子的指针 */
int type; /* value 的类型 */
char *valuestring; /* 如果这个 value 是 字符串 的话,字符串值 */
int valueint; /* 如果是数字的话,整数值 */
double valuedouble; /* 如果是数字的话,浮点数值 */
char *string; /* 如果是对象的 key-value 元素的话, key 值 */
type字段的类型,表示的当前json结点的数据类型:
#define cJSON_False 0
#define cJSON_True 1
#define cJSON_NULL 2
#define cJSON_Number 3 #数字,注意这个数字表示的并不是int类型的数组,而是字符串类型的数字
#define cJSON_String 4 #字符串
#define cJSON_Array 5 #数组
#define cJSON_Object 6 #嵌套json的类型
如果是对象或者数组,采用的是双向链表来实现,链表中的每一个节点表示数组中的一个元素或者对象中的一个字段。其中child表示头结点,next、prev分别表示下一个节点和前一个节点。valuestring、valueint、valuedouble分别表示字符串、整数、浮点数的字面量。
一个如下的json,如果用这个数据结构去表示的话,结构如下:
如果是一个json的嵌套,那么结构如下所示:
2. 内存管理
内存管理指的是创建一个JSON结点和删除一个JSON结点,创建结点就是使用malloc内存分配,删除结点就判断一下JSON的type,如果是基本类型就直接释放,如果是数组或者对象就递归删除(类似二叉树)。
//创建结点
static cJSON *cJSON_New_Item(void) {
cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON));
if (node) memset(node,0,sizeof(cJSON));
return node;
}
//删除结点,注意看递归删除的部分
void cJSON_Delete(cJSON *c) {
cJSON *next;
while (c) {
next=c->next;
if (!(c->type&cJSON_IsReference) && c->child) cJSON_Delete(c->child);
if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring);
if (c->string) cJSON_free(c->string);
cJSON_free(c);
c=next;
}
}
3. json解析
json解析的功能实现的是给出一个字符串,然后把它解析成一个cjson的数据结构的类型。
解析的核心代码:
//根据字符串来判断当前json结点的类型
static const char *parse_value(cJSON *item,const char *value) {
if (!value)return 0;/* Fail on null. */
if (!strncmp(value,"null",4)) {
item->type=cJSON_NULL;
return value+4;
}
if (!strncmp(value,"false",5)) {
item->type=cJSON_False;
return value+5;
}
if (!strncmp(value,"true",4)) {
item->type=cJSON_True;
item->valueint=1;
return value+4;
}
//字符串类型
if (*value=='\"') {
return parse_string(item,value);
}
//数字类型
if (*value=='-' || (*value>='0' && *value<='9')) {
return parse_number(item,value);
}
//数组类型
if (*value=='[') {
return parse_array(item,value);
}
//object类型
if (*value=='{') {
return parse_object(item,value);
}
ep=value;
return 0;/* failure. */
}
对于不同的字符串,调用不同的解析函数。贴一个比较复杂的解析函数,就是解析object,分析如下:
static const char *parse_object(cJSON *item,const char *value)
{
printf("********* parse_object \n");
cJSON *child;
if (*value!='{') {ep=value;return 0;} /* not an object! */
item->type=cJSON_Object;
value=skip(value+1);
if (*value=='}') return value+1; /* empty array. */
//给item 创建一个子节点child
item->child=child=cJSON_New_Item();
if (!item->child) return 0;
//子节点 解析字符串,执行之后,child->type=cjson_string;child->valuestring="name" ,parse_string的返回值=":\"zhao\",\"age\":18" 是剩下的字符串
value=skip(parse_string(child,skip(value)));
if (!value) return 0;
// child->string 在这里想表达的是json里面的key
child->string=child->valuestring;
child->valuestring=0;
if (*value!=':') {ep=value;return 0;} /* fail! */
// 继续解析字符串里面的value的部分
value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */
if (!value) return 0;
while (*value==',')
{
cJSON *new_item;
if (!(new_item=cJSON_New_Item())) return 0; /* memory fail */
child->next=new_item;
new_item->prev=child;
child=new_item;
value=skip(parse_string(child,skip(value+1)));
if (!value) return 0;
child->string=child->valuestring;
child->valuestring=0;
if (*value!=':') {ep=value;return 0;} /* fail! */
value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */
if (!value) return 0;
}
if (*value=='}') return value+1; /* end of array */
ep=value;return 0; /* malformed. */
}
4. 对json结点的操作
对结点的操作,我觉得可以抽象的理解为一棵树(多叉树),这样对结点的操作基本上和树对结点的操作是相似的。
4.1 添加结点
4.2 删除结点
4.3 查找结点
4.4 修改结点
5. 输出和序列化
cjson 的做法不是边分析 json 边输出, 而是预先将要输的内容全部按字符串存在内存中, 最后输出整个字符串。所以,看代码的时候,可以顺着这个思路去看。
PS
最近用写论文空余的时间,连着看了三个经典的开源项目,按代码量从小到大,分别是webbench,tinyhttp,cjson。感觉受益匪浅,从这些前辈的代码里面可以快速的学到这门语言的最佳实践,解决问题的思路,coding的风格。
再有一点,体会很深的一点是,这几个开源的项目里面,大量的代码都是为了考虑解决异常的情况,比如输入异常,故意输入错误如何处理;创建进程失败,如何处理;创建管道出错怎么办;内存分配失败怎么办等等。这些繁杂的工程细节会让人厌烦,但不得不承认,这是一个优秀代码的必须要有的东西。
如何在国外的网站下载源码不方便,可以移步我的github:
与源码相比,我在github里面的版本插入了很多printf来输出,应该可以更容易理解一点吧
https://github.com/zhaozhengcoder/rebuild-the-wheel/tree/master/cJSON/src
参考文献 :
http://blog.csdn.net/yzhang6_10/article/details/51615089
http://www.jianshu.com/p/838f69db2f71
http://github.tiankonguse.com/blog/2014/12/18/cjson-source.html
others
webbench的源码:http://www.jianshu.com/p/e65c7efc96aa
tinyhttp的源码:http://www.jianshu.com/p/18cfd6019296