本文采用以下协议进行授权: 自由转载-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。

cJSON是一个超轻巧,携带方便,单文件,简单的可以作为ANSI-C标准的Json格式解析库。

那什么是Json格式?这里照搬度娘百科的说法:

Json(JavaScript Object Notation)
是一种轻量级的数据交换格式。它基于JavaScript(Standard ECMA-262 3rd Edition – December
1999)的一个子集。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java,
JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成。

更加详细的解释和示例请查看 http://www.json.org/
主页。

其实简单说,Json就是一种信息交换格式,而cJSON其实就是对Json格式的字符串进行构建和解析的一个C语言函数库。

可以在以下地址下载到cJSON的源代码:

http://sourceforge.net/projects/cjson/

__MACOSX目录是提供给Mac OS的源码,我的机器运行的是Fedora 18,所以选择另外一个目录即可。

简单的阅读下README文件,先学习cJSON库的使用方法。若是连库都还不会使用,分析源码就无从谈起了。通过简单的了解,我们得知cJSON库实际上只有cJSON.c和cJSON.h两个文件组成,绝对轻量级。

不过,代码风格貌似有点非主流,先用indent格式化一下代码吧。我个人喜欢K&R风格的代码,使用的indent命令行参数如下:

indent -bad -bli 0 -ce -kr -nsob --space-after-if --space-after-while --space-after-for --use-tabs -i8
1

indent - bad - bli 0 - ce - kr - nsob -- space - after - if -- space - after - while -- space - after - for -- use - tabs - i8

格式化之后,代码结构看起来清晰多了。

那么,从何处下手来分析呢?打开代码文件逐行阅读么?当然不是了,有main函数的程序大都是从main函数开始分析,那么没有main函数的纯函数库呢?那就自己写main函数呗。

cJSON作为Json格式的解析库,其主要功能无非就是构建和解析Json格式了,我们先写一个构建Json格式字符串的程序,尽可能的使其用到的类型多一点(事实上README文件里提供了不错的示例代码,我们直接借鉴一下吧)。代码如下:

#include <stdio.h>
#include <stdlib.h>

#include "cJSON.h"

int main(int argc, char *argv[])
{
cJSON *root, *fmt;

root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "name", "Jack (\"Bee\") Nimble");
fmt = cJSON_CreateObject();
cJSON_AddItemToObject(root, "format", fmt);
cJSON_AddStringToObject(fmt, "type", "rect");
cJSON_AddNumberToObject(fmt, "width", 1920);
cJSON_AddFalseToObject(fmt, "interlace");

char *result = cJSON_Print(root);
puts(result);

free(result);
cJSON_Delete(root);

return EXIT_SUCCESS;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#include <stdio.h>

#include <stdlib.h>

    

#include "cJSON.h"

    

int main ( int argc , char * argv [ ] )

{

cJSON * root , * fmt ;

            

root = cJSON_CreateObject ( ) ;

cJSON_AddStringToObject ( root , "name" , "Jack (\"Bee\") Nimble" ) ;

fmt = cJSON_CreateObject ( ) ;

cJSON_AddItemToObject ( root , "format" , fmt ) ;

cJSON_AddStringToObject ( fmt , "type" , "rect" ) ;

cJSON_AddNumberToObject ( fmt , "width" , 1920 ) ;

cJSON_AddFalseToObject ( fmt , "interlace" ) ;

    

char * result = cJSON_Print ( root ) ;

puts ( result ) ;

            

free ( result ) ;

cJSON_Delete ( root ) ;

    

return EXIT_SUCCESS ;

}

编译运行后 ( 编译时注意要链接数学库,参数行要加 -lm) ,运行结果如下:

{
"name": "Jack (\"Bee\") Nimble",
"format": {
"type": "rect",
"width": 1920,
"interlace": false
}
}
1
2
3
4
5
6
7
8
{

"name" : "Jack (\"Bee\") Nimble" ,

     "format" : {

"type" : "rect" ,

"width" : 1920 ,

"interlace" : false

     }
}

打开cJSON.h这个头文件,我们可以看到每一个节点,实际上都是由cJSON这个结构体来描述的:

typedef struct cJSON {
struct cJSON *next, *prev;
struct cJSON *child;

int type;

char *valuestring;
int valueint;
double valuedouble;

char *string;
} cJSON;

1
2
3
4
5
6
7
8
9
10
11
12

typedef struct cJSON {

struct cJSON * next , * prev ;

struct cJSON * child ;

    
     int type ;
    

char * valuestring ;

int valueint ;

double valuedouble ;

    

char * string ;

} cJSON ;

结合这个结构体和上面相关API的调用,其实我们大概可以猜测出cJSON对于Json格式的描述和处理的方法了:

每一个cJSON结构都描述了一项”键-值”对的数据,其中next和prev指针显然是指向同级前后的cJSON结构,而child指针自然是指向孩子节点的cJSON结构。type类型显然是为了区分值的类型而设置的,在cJSON.h文件一开始就定义了这些类型的值:

/* cJSON Types: */
#define cJSON_False 0
#define cJSON_True 1
#define cJSON_NULL 2
#define cJSON_Number 3
#define cJSON_String 4
#define cJSON_Array 5
#define cJSON_Object 6
1
2
3
4
5
6
7
8

/* cJSON Types: */

#define cJSON_False  0

#define cJSON_True   1

#define cJSON_NULL   2

#define cJSON_Number 3

#define cJSON_String 4

#define cJSON_Array  5

#define cJSON_Object 6

很显然通过检测这里的type字段,就很容易知道该节点的类型以及其实际存储数据的字段了。其它的字段是什么意思呢?cJSON.h文件里的注释说的很明白了,valueint,valuedouble以及valuestring保存的是相应的值,string存放的是本字段的名字。

接下来分析程序的执行过程,编译参数加上-g,使用gdb调试程序,画出整个构造过程的函数调用图。具体的调试过程就不细说了,我捡一些关键点说说:

调试过程中,我们发现 cJSON_AddStringToObject() 等其实是宏定义,本质上调用的都是 cJSON_AddItemToObject() 函数,在cJSON.h文件中可以看到如下定义:

#define cJSON_AddNullToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateNull())
#define cJSON_AddTrueToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateTrue())
#define cJSON_AddFalseToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateFalse())
#define cJSON_AddBoolToObject(object,name,b) cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))
#define cJSON_AddNumberToObject(object,name,n) cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))
#define cJSON_AddStringToObject(object,name,s) cJSON_AddItemToObject(object, name, cJSON_CreateString(s))
1
2
3
4
5
6

#define cJSON_AddNullToObject(object,name)      cJSON_AddItemToObject(object, name, cJSON_CreateNull())

#define cJSON_AddTrueToObject(object,name)      cJSON_AddItemToObject(object, name, cJSON_CreateTrue())

#define cJSON_AddFalseToObject(object,name)     cJSON_AddItemToObject(object, name, cJSON_CreateFalse())

#define cJSON_AddBoolToObject(object,name,b)    cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))

#define cJSON_AddNumberToObject(object,name,n)  cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))

#define cJSON_AddStringToObject(object,name,s)  cJSON_AddItemToObject(object, name, cJSON_CreateString(s))

另外 cJSON_CreateNull() 等函数都是调用 cJSON_New_Item() 函数申请到初始化为0的空间构造相关的节点信息。构造过程中的函数调用图如下:

构造的Json字符串最终在内存中形成的结构如下图所示:

构造过程相对来说比较简单,数组类型这里没有涉及到,但是分析起来也很简单。

我们最后调用 cJSON_Print()
函数生成这个结构所对应的字符串。生成说起来容易,遍历起整个结构并进行字符串格式控制却比较繁琐。这里相关的代码还有递归清理这个内存结构的函数不再赘述,有兴趣的同学请自行研究。

构造的过程我们就说到这里,明天我们研究下解析的过程。


昨天简单的分析了一下cJSON对Json格式的构造过程,今天仔细读了读README文件,发现README其实说的已经很详细了。重复造轮子就重复造轮子吧,今天我们再一起分析解析的过程。

继续用之前构造的Json格式来进行解析,之前分析构造函数的时候,我们只是简单的分析了几个cJSON结构的构造过程,并没有涉及到各种类型的数组等构造。因为我觉得理解了一般的构造过程,更复杂的类型自己再简单看看源码,画画图就很容易理解。

 

学习一个事物一定要先抓住主线,先掌握一个事物最常用的那50%,其他的边边角角完全可以留给实践去零敲碎打(孟岩语)。

闲话打住,先上一段解析使用的代码:

#include <stdio.h>
#include <stdlib.h>
  
#include "cJSON.h"
  
int main(int argc, char *argv[])
{
  char *text = "{\"name\": \"Jack (\\\"Bee\\\") Nimble\", "
   "\"format\": {\"type\": \"rect\", "
   "\"width\": 1920, \"interlace\": false}}";
  
  cJSON *root = cJSON_Parse(text);
  if (!root) {
    printf("Error before: [%s]\n", cJSON_GetErrorPtr());
    return EXIT_FAILURE;
  }
  
  char *out = cJSON_Print(root);
  printf("text:\n%s\n\n", out);
  free(out);
  
  char *name = cJSON_GetObjectItem(root, "name")->valuestring;
  printf("name : %s\n", name);
  cJSON *format = cJSON_GetObjectItem(root, "format");
  int width = cJSON_GetObjectItem(format, "width")->valueint;
  printf("width : %d\n", width);
  
  cJSON_Delete(root);
  
  return EXIT_SUCCESS;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

#include <stdio.h>

#include <stdlib.h>

#include "cJSON.h"

int main ( int argc , char * argv [ ] )

{

char * text = "{\"name\": \"Jack (\\\"Bee\\\") Nimble\", "

"\"format\": {\"type\": \"rect\", "

"\"width\": 1920, \"interlace\": false}}" ;

cJSON * root = cJSON_Parse ( text ) ;

if ( ! root ) {

printf ( "Error before: [%s]\n" , cJSON_GetErrorPtr ( ) ) ;

return EXIT_FAILURE ;

}

char * out = cJSON_Print ( root ) ;

printf ( "text:\n%s\n\n" , out ) ;

free ( out ) ;

char * name = cJSON_GetObjectItem ( root , "name" ) -> valuestring ;

printf ( "name : %s\n" , name ) ;

cJSON * format = cJSON_GetObjectItem ( root , "format" ) ;

int width = cJSON_GetObjectItem ( format , "width" ) -> valueint ;

printf ( "width : %d\n" , width ) ;

cJSON_Delete ( root ) ;

return EXIT_SUCCESS ;

}

程序运行输出:

text:
{
  "name": "Jack (\"Bee\") Nimble",
  "format":{
    "type": "rect",
    "width": 1920,
    "interlace": false
  }
}
  
name : Jack ("Bee") Nimble
width : 1920
1
2
3
4
5
6
7
8
9
10
11
12
text :
{

"name" : "Jack (\"Bee\") Nimble" ,

"format" : {
"type" : "rect" ,
"width" : 1920 ,

"interlace" : false

}
}

name : Jack ( "Bee" ) Nimble

width : 1920

从这段代码中可以看到,解析过程就 cJSON_Parse() 一个接口,调用成功返回cJSON结构体的指针,错误返回NULL,此时调用 cJSON_GetErrorPtr() 可以得要错误原因的描述字符串。

查看 cJSON_GetErrorPtr() 的源码可以得知,其实错误信息就保存在全局字符串指针ep里。

关键就是对 cJSON_Parse() 过程的分析了,我们带参数-g重新编译代码并下断点开始调试跟踪。

首先 cJSON_Parse() 调用 cJSON_New_Item() 申请一个新的cJSON节点,然后使用函数对输入字符串进行解析(中间使用了 skip() 函数来跳过空格和换行符等字符)。

         parse_value() 函数对输入字符串进行匹配和解析,检测输入数据的类型并调用 parse_string()parse_number()parse_array()parse_object() 等函数进行解析,然后返回结束的位置。

函数调用的关系如下图:

这些函数之间相互调用,传递待解析的字符串直到结束或者遇见错误便返回,最后会构建出一个和之前结构一样的Json内存结构来,解析的过程就完成了。检索过程很简单 cJSON_GetObjectItem()
函数负责进行某个对象的自成员的名字比对和指针的返回。不过要注意这里采用了 cJSON_strcasecmp()
这个无视大小写的字符串比较函数,因为Json格式的键值对的名称不区分大小写。

这样cJSON库的整个构建和解析过程的主干内容就总结出来了,剩下的边边角角可以在这个主线分析结束之后再继续下去,比如Json格式化,解析出来的内存结构复制,从这个内存结构解析出字符串以及这个内存结构的递归删除等等留给大家自己进行吧。

P.S. cJSON_InitHooks()
这个函数不过是cJSON允许用户使用其它的内存申请和释放函数罢了(默认是malloc和free),另外啰嗦一下,这个接口也可以用来检测内存泄露。只要实现malloc和free的包装函数,在其中统计和打印内存申请释放操作就可以了。

若无特别声明,本站文章皆为原创,转载时烦请注明:
转载自
浅墨的博客

cJSON库源码分析的更多相关文章

  1. Redis网络库源码分析(1)之介绍篇

    一.前言 Redis网络库是一个单线程EPOLL模型的网络库,和Memcached使用的libevent相比,它没有那么庞大,代码一共2000多行,因此比较容易分析.其实网上已经有非常多有关这个网络库 ...

  2. springBoot集成Redis遇到的坑(择库)源码分析为什么择库失败

    提示: springboot提供了一套链接redis的api,也就是个jar包,用到的连接类叫做LettuceConnectionConfiguration,所以我们引入pom时是这样的 <de ...

  3. 经典iOS第三方库源码分析 - YYModel

    YYModel介绍 YYModel是一个针对iOS/OSX平台的高性能的Model解析库,是属于YYKit的一个组件,创建是ibireme. 其实在YYModel出现之前,已经有非常多的Model解析 ...

  4. AspNetCore.AsyncInitialization库源码分析

    AspNetCore.AsyncInitialization 这个库是用来实现在asp.net core应用程序启动时异步执行异步任务.可参考:如何在ASP.NET Core程序启动时运行异步任务(2 ...

  5. Redis事件库源码分析

    由于老大在新项目中使用redis的事件库代替了libevent,我也趁着机会读了一遍redis的事件库代码,第一次读到“优美,让人愉快”的代码,加之用xmind制作的类图非常帅,所以留文纪念. Red ...

  6. Redis网络库源码分析(3)之ae.c

    一.aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我们已经将服务器启动,只是其中有些细节我们跳过了,比如aeCreateEventLoop函数到底做 ...

  7. Redis网络库源码分析(2)之启动服务器

    一.从main开始 main函数定义在server.c中,它的内容如下: //server.c int main() { signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信号 ...

  8. Redis 专栏(使用介绍、源码分析、常见问题...)

    一.介绍相关 说Redis : 介绍Redis特性,使用场景,使用Jedis操作Redis等. 二.源码分析 1. 数据结构 Redis源码分析(sds):Redis自己封装的C语言字符串类型. Re ...

  9. cJSON源码分析

    JSON (JavaScript Object Notation) 是一种常见使用的轻量级的数据交换格式,既有利于人工读写,也方便于机器的解析和生成. 关于JSON格式的定义,参看网站[1].在该网站 ...

随机推荐

  1. 选择IM云服务供应商

    选择IM云服务供应商,其实最重要是有三个因素:费用.技术稳定性.以及后续运维服务. 对于不少创业公司来讲,可能需要找到成本和稳定性的最佳平衡点.目前国内不少IM云服务产品都推出了免费服务项目或者一定期 ...

  2. Oracle Merge 使用

    <转自> http://blog.csdn.net/nsj820/article/details/5755685 Oracle9i引入了MERGE命令,你能够在一个SQL语句中对一个表同时 ...

  3. vue 权限管理深度探究

    实现思路如下:1.网页路由(route)中定义的每个路由都有meta属性,属性值防止可访问该路由的值.2.路由的全局前置守卫(beforeEach)会判断路由用户是否登录(未登录跳转至登录界面),以及 ...

  4. 关于表格合并span-method方法的补充(表格数据由后台动态返回)

    之前写了一些关于element-ui表格合并的方法,不过用的数据都是确定的数据(死数据),但是很多时候我们的数据都是通过后台获得的,数据不稳定,这个时候使用表格合并就需要先处理一下数据,先看一下一种很 ...

  5. http和web缓存

    1.http的缓存类型   缓 存对于一个网站来说非常重要,可以提高网站性能,减少冗余的数据传输,增加服务器负担,web存储则给浏览器提供了更加强大的保存文件的接口.关于web下的http缓存类型比较 ...

  6. zabbix 另一种方式取 zabbix-sender

    一,zabbix-sender介绍 这种模式是两主机并没有agent互联 使用zabbix-serder的话适用那种没有固定公网IP的,实时系统数据监控操作 还一个实用为零延迟数据监控, 本省zabb ...

  7. LINUX学习之一:

    学好linux的基础:C语言(GNU C语言与GCC):硬件基础:熟悉操作系统内核代码,熟悉多线程和网络知识.分驱动开发(驱动程序模型即框架)和应用程序开发,目标是驱动开发 驱动开发特点: 不能使用标 ...

  8. Linux 上安装 weblogic12C (远程图形界面安装) (二)

    上一篇Linux 上安装 weblogic12C (静默安装)介绍了静默方式安装weblogic12C的方式,这一篇主要介绍在windows主机上通过远程图形界面的方式安装weblogic的方式 一. ...

  9. 使用jxl读取excel内容,并转换成Json,用于Datagrid

    一.上传excel文件,得到InputStream,由InputStream得到Jxl中的Workbook,取出内容,存到二维数组中. 1.使用 Jquery Uploadify 插件(http:// ...

  10. BNU27935——我爱背单词——————【数组模拟】

    我爱背单词 Time Limit: 1000ms Memory Limit: 65536KB 64-bit integer IO format: %lld      Java class name:  ...