JSON的C语言解析库---cJSON与jsmn及其应用

问题描述

在只有64K甚至只有20K这样KB级别的内存资源极其有限的单片机中,要解析如下这种复杂的多级嵌套结构的json数据是很头疼的事。常用的cJSON解析器是通过链表将json数据按照键值对的形式展开,在解析过程中会大量的动态申请内存。在rtthread操作系统中,每一次malloc都会携带一个控制块,用于对申请内存的管理,在频繁malloc时,控制块的内存占用会比实际要申请得内存要大得多

{
	"u1_cfg": [115200, 8, 1, 0, 1000],
	"u2_cfg": [115200, 8, 1, 0, 1000],
	"u1_devcfg": [
		[1, "2222222", 1, "yytt"],
		[3, "3333333", 1, "yytt"]
	],
	"u2_devcfg": [
		[0, "4444444", 1, "testtool"]
	],
	"protocol": [{
		"tsl": "yytt",
		"devid": 1,
		"sn": "2222222",
		"name": "modbus_rtu",
		"item": [
			["addr", 40000, "us", 1, 2, 2, 0, 0, 0, 0],
			...
			["Humit4", 40142, "f", 4, 2, 2, 0, 0, 0, 0]
		]
	}, {
		"tsl": "yytt",
		"devid": 3,
		"sn": "3333333",
		"name": "modbus_rtu",
		"item": [
			["addr", 40000, "us", 1, 2, 2, 0, 0, 0, 0],
			...
			["Humit4", 40142, "f", 4, 2, 2, 0, 0, 0, 0]
		]
	}, {
		"tsl": "testtool",
		"devid": 0,
		"sn": "4444444",
		"name": "testtool",
		"item": [
			[]
		]
	}]
}

经测试每增加一组item数据,解析时内存占用就会多672字节

要解决这个问题,有2种思路

  1. 限制json数据的嵌套深度和数据大小,但是如果限制数据量大小还得考虑分包,太简单的结构表达不了这么复杂的数据
  2. 采用不必展开的JSON解析器比如jsmn,不使用cJSON这种展开式动态申请内存的解析器

下面就对JSON及cJSON、jsmn解析器的使用进行说明

一、JSON

JSON 全称 JavaScript Object Notation,即 JS对象简谱,是一种轻量级的数据格式

它采用完全独立于编程语言文本格式来存储和表示数据,语法简洁、层次结构清晰,易于人阅

读和编写,同时也易于机器解析和生成,有效的提升了网络传输效率。

1.1、JSON语法规则

JSON对象是一个无序的"名称/值"键值对的集合:

  • 以"{“开始,以”}"结束,允许嵌套使用;
  • 每个名称和值成对出现,名称和值之间使用":"分隔;
  • 键值对之间用","分隔
  • 在这些字符前后允许存在无意义的空白符;

对于键值,可以有如下类型值:

  • 一个新的json对象object
  • 数组array:使用"[“和”]"表示
  • 数字number:直接表示,可以是整数,也可以是浮点数
  • 字符串string:使用引号"表示
  • 布尔值bool:false、null、true中的一个(必须是小写)
{
    "name": "mculover666",
    "age": 22,
    "weight": 55.5
    "address":
    {
        "country": "China",
        "zip-code": 111111
    },
    "skill": ["c", "Java", "Python"],
    "student": false
}

1.2、JSON的C语言解析库

JSON的官网介绍中可以找着很多不同编程语言的关于JSON解析的开源库,C语言比较有名的常用的解析器有 cJSON 和 jsmn,这两个协议,jsmn 特别适用于单片机中内存资源极其有限的环境,一个资源占用极小的 JSON 解析器,号称世界上最快;cJSON 适合空间比较充足,需要大型数据处理的环境。

二、cJSON

cJSON 是一个使用C语言编写的JSON数据解析器,具有超轻便,可移植,单文件的特点,只有cJSON.c和cJSON.h两个文件,使用时将这两个文件添加到工程即可

2.1、cJSON对JSON数据的封装

cJSON使用cJSON结构体来表示一个JSON数据,定义在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
	
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
	
/* The cJSON structure: */
typedef struct cJSON {
	struct cJSON *next,*prev;	/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
	struct cJSON *child;		/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */

	int type;					/* The type of the item, as above. */

	char *valuestring;			/* The item's string, if type==cJSON_String */
	int valueint;				/* The item's number, if type==cJSON_Number */
	double valuedouble;			/* The item's number, if type==cJSON_Number */

	char *string;				/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
} cJSON;

typedef struct cJSON_Hooks {
      void *(*malloc_fn)(size_t sz);
      void (*free_fn)(void *ptr);
} cJSON_Hooks;

/* Supply malloc, realloc and free functions to cJSON */
extern void cJSON_InitHooks(cJSON_Hooks* hooks);

/* Supply a block of JSON, and this returns a cJSON object you can interrogate. Call cJSON_Delete when finished. */
extern cJSON *cJSON_Parse(const char *value);
/* Render a cJSON entity to text for transfer/storage. Free the char* when finished. */
extern char  *cJSON_Print(cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. Free the char* when finished. */
extern char  *cJSON_PrintUnformatted(cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
extern char *cJSON_PrintBuffered(cJSON *item,int prebuffer,int fmt);
/* Delete a cJSON entity and all subentities. */
extern void   cJSON_Delete(cJSON *c);

/* Returns the number of items in an array (or object). */
extern int cJSON_GetArraySize(cJSON *array);
/* Retrieve item number "item" from array "array". Returns NULL if unsuccessful. */
extern cJSON *cJSON_GetArrayItem(cJSON *array,int item);
/* Get item "string" from object. Case insensitive. */
extern cJSON *cJSON_GetObjectItem(cJSON *object,const char *string);

/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
extern const char *cJSON_GetErrorPtr(void);
	
/* These calls create a cJSON item of the appropriate type. */
extern cJSON *cJSON_CreateNull(void);
extern cJSON *cJSON_CreateTrue(void);
extern cJSON *cJSON_CreateFalse(void);
extern cJSON *cJSON_CreateBool(int b);
extern cJSON *cJSON_CreateNumber(double num);
extern cJSON *cJSON_CreateString(const char *string);
extern cJSON *cJSON_CreateArray(void);
extern cJSON *cJSON_CreateObject(void);

/* These utilities create an Array of count items. */
extern cJSON *cJSON_CreateIntArray(const int *numbers,int count);
extern cJSON *cJSON_CreateFloatArray(const float *numbers,int count);
extern cJSON *cJSON_CreateDoubleArray(const double *numbers,int count);
extern cJSON *cJSON_CreateStringArray(const char **strings,int count);

/* Append item to the specified array/object. */
extern void cJSON_AddItemToArray(cJSON *array, cJSON *item);
extern void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item);
extern void cJSON_AddItemToObjectCS(cJSON *object,const char *string,cJSON *item);	/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object */
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
extern void cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
extern void cJSON_AddItemReferenceToObject(cJSON *object,const char *string,cJSON *item);

/* Remove/Detatch items from Arrays/Objects. */
extern cJSON *cJSON_DetachItemFromArray(cJSON *array,int which);
extern void   cJSON_DeleteItemFromArray(cJSON *array,int which);
extern cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string);
extern void   cJSON_DeleteItemFromObject(cJSON *object,const char *string);
	
/* Update array items. */
extern void cJSON_InsertItemInArray(cJSON *array,int which,cJSON *newitem);	/* Shifts pre-existing items to the right. */
extern void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem);
extern void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);

/* Duplicate a cJSON item */
extern cJSON *cJSON_Duplicate(cJSON *item,int recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
need to be released. With recurse!=0, it will duplicate any children connected to the item.
The item->next and ->prev pointers are always zero on return from Duplicate. */

/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
extern cJSON *cJSON_ParseWithOpts(const char *value,const char **return_parse_end,int require_null_terminated);

extern void cJSON_Minify(char *json);

/* Macros for creating things quickly. */
#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))

/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object,val)    ((object)?(object)->valueint=(object)->valuedouble=(val):(val))
#define cJSON_SetNumberValue(object,val)    ((object)?(object)->valueint=(object)->valuedouble=(val):(val))

cJSON的设计很巧妙

首先,它不是将一整段JSON数据抽象出来,而是将其中的一条JSON数据抽象出来,也就是一个键值对,用上面的结构体 strcut cJSON 来表示

其次,一段完整的JSON数据中由很多键值对组成,并且涉及到键值对的查找、删除、添加,所以使用链表来存储整段JSON数据,如上面的代码所示:

  • next指针:指向下一个键值对
  • prev指针指向上一个键值对

最后,因为JSON数据支持嵌套,所以一个键值对的值会是一个新的JSON数据对象(一条新的链表),也有可能是一个数组,方便起见,在cJSON中,数组也表示为一个数组对象,用链表存储,所以,在键值对结构体中,当该键值对的值是一个嵌套的JSON数据或者一个数组时,由child指针指向该条新链表。

2.2、cJSON使用注意事项

  • cJSON的所有操作都是基于链表的,所以cJSON在使用过程中大量的使用malloc从堆中分配动态内存的,所以在使用完之后,应当及时调用 (void) cJSON_Delete(cJSON *item),清空cJSON指针所指向的内存

注意:该函数删除一条JSON数据时,如果有嵌套,会连带删除。

  • cJSON组织的浮点数据会默认保留6位小数,而这6位小数通过cJSON_PrintUnformatted组织成text后会占用内存的,其实很没有必要。还会cJSON_malloc 64字节的空间,很浪费
  • cJSON组织的整形数据,为表示2^64,会cJSON_malloc 21字节的空间,很浪费。在32为系统中,表示到2^32就可以了,所以可以改为11
/* Render the number nicely from the given item into a string. */
static char *print_number(cJSON *item,printbuffer *p)
{
	char *str=0;
	double d=item->valuedouble;
	if (d==0)
	{
		if (p)	str=ensure(p,2);
		else	str=(char*)cJSON_malloc(2);	/* special case for 0. */
		if (str) strcpy(str,"0");
	}
	else if (fabs(((double)item->valueint)-d)<=DBL_EPSILON && d<=INT_MAX && d>=INT_MIN)
	{
		if (p)	str=ensure(p,21);
		else str=(char*)cJSON_malloc(21);	/* 2^64+1 can be represented in 21 chars. */
		if (str) sprintf(str,"%d",item->valueint);
	}
	else
	{
		if (p)	str=ensure(p,64);
		else	str=(char*)cJSON_malloc(64);	/* This is a nice tradeoff. */
		if (str)
		{
			if (fabs(floor(d)-d)<=DBL_EPSILON && fabs(d)<1.0e60)sprintf(str,"%.0f",d);
			else if (fabs(d)<1.0e-6 || fabs(d)>1.0e9)sprintf(str,"%e",d);
			else sprintf(str,"%g",d); //原来为%f,这里改为%g
		}
	}
	return str;
}

三、jsmn

jsmn,一个资源占用极小的json解析器,号称世界上最快。jsmn主要有以下特性:

  • 没有任何库依赖关系;
  • 语法与C89兼容,代码可移植性高;
  • 没有任何动态内存分配
  • 极小的代码占用
  • API只有两个,极其简洁

3.1、jsmn对JSON数据项的抽象

jsmn对json数据中的每一个数据段都会抽象为一个结构体,称之为token,此结构体非常简洁:

/**
 * JSON type identifier. Basic types are:
 *  o Object
 *  o Array
 *  o String
 *  o Other primitive: number, boolean (true/false) or null
 */
typedef enum
{
    JSMN_UNDEFINED = 0,
    JSMN_OBJECT = 1,
    JSMN_ARRAY = 2,
    JSMN_STRING = 3,
    JSMN_PRIMITIVE = 4
} jsmntype_t;

enum jsmnerr
{
    /* Not enough tokens were provided */
    JSMN_ERROR_NOMEM = -1,
    /* Invalid character inside JSON string */
    JSMN_ERROR_INVAL = -2,
    /* The string is not a full JSON packet, more bytes expected */
    JSMN_ERROR_PART = -3
};

/**
 * JSON token description.
 * type     type (object, array, string etc.)
 * start    start position in JSON data string
 * end      end position in JSON data string
 */
typedef struct
{
    jsmntype_t type;
    int start;
    int end;
    int size;
#ifdef JSMN_PARENT_LINKS
    int parent;
#endif
} jsmntok_t;

/**
 * JSON parser. Contains an array of token blocks available. Also stores
 * the string being parsed now and current position in that string
 */
typedef struct
{
    unsigned int pos; /* offset in the JSON string */
    unsigned int toknext; /* next token to allocate */
    int toksuper; /* superior token node, e.g parent object or array */
} jsmn_parser;

/**
 * Create JSON parser over an array of tokens
 */
void jsmn_init(jsmn_parser *parser);

/**
 * Run JSON parser. It parses a JSON data string into and array of tokens, each describing
 * a single JSON object.
 */
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
               jsmntok_t *tokens, unsigned int num_tokens);

从结构体中的数据成员可以看出,jsmn并不保存任何具体的数据内容,仅仅记录:

  • 数据项的类型
  • 数据项数据段在原始json数据中的起始位置
  • 数据项数据段在原始json数据中的结束位置

3.2、jsmn如何解析出每个token

上述说到jsmn将每一个json数据段都抽象为一个token,那么jsmn是如何对整段json数据进行解析,得到每一个数据项的token呢?

jsmn解析就是将json数据逐个字符进行解析,用pos数据成员来记录解析器当前的位置,当寻找到特殊字符时,就去之前我们定义的token数组(t)中申请一个空的token成员,将该token在数组中的位置记录在数据成员toknext中。这整个解析过程只需要调用 int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, unsigned int num_tokens);

比如要解析这么一串json数据

{"name":"mculover666","admin":false,"uid":1000}

每个token的 type、start、end、size 如下:

这段json数据解析出的token有7个:

① Object类型的token:{"name":"mculover666","admin":false,"uid":1000}

② String类型的token:"name"、"mculover666"、"admin"、"uid"

③ number类型的token:数字1000,布尔值false

3.3、用户如何从token中提取值

在解析完毕获得这些token之后,需要根据token数量来判断是否解析成功:

① 返回的token数量

enum jsmnerr {
  /* Not enough tokens were provided */
  JSMN_ERROR_NOMEM = -1,
  /* Invalid character inside JSON string */
  JSMN_ERROR_INVAL = -2,
  /* The string is not a full JSON packet, more bytes expected */
  JSMN_ERROR_PART = -3
};

② 判断第0个token是否是JSMN_OBJECT类型,如果不是,则证明解析错误。

③ 如果token数量大于1,则从第1个token开始判断字符串是否与给定的键值对的名称相等,若相等,则提取下一个token的内容作为该键值对的值。

3.4、参照cJSON的API实现相同功能

jsmn原生没有提供跟cJSON解析库类似的简单易用的json对象操作函数,如果只用jsmn提供的2个API函数来解析JSON数据还是有难度的,rtthread中有实现好的软件包,操作API如下

typedef struct
{
    jsmntok_t *t; /**< 对应 jsmn token 对象 */
    int index;    /**< 对应 jsmn token 对象在 token 数组中的索引 */
    int left_num; /**< token 数组中剩余 token 数目 */
} jsmn_item_t;


void JSMN_ItemInit(jsmn_item_t *item, jsmntok_t *t, int index, int tokens_len);
int JSMN_GetObjectItem(const char *js, jsmn_item_t *object, const char *const string, jsmn_item_t *item);
char *JSMN_GetString(char *js, jsmn_item_t *item);
int JSMN_GetStringBuffered(const char *js, jsmn_item_t *item, char *buf, int bufsz);
char *JSMN_GetValueString(char *js, jsmn_item_t *item);
int JSMN_GetValueStringBuffered(const char *js, jsmn_item_t *item, char *buf, int bufsz);
int JSMN_GetArraySize(jsmn_item_t *array);
int JSMN_GetArrayItem(jsmn_item_t *array, int index, jsmn_item_t *item);

示例:解析如下json数据

{
	"fmt": "J",
	"type": 1, 
	"id": "20210520101059001",
	"protocol_ver": "1.0", 
	"sn": "22222222",
	"tsl": "gw", 
	"data":{
		"act_ty": "reboot", 
		"delay":"1"
	} 
}

解析代码如下:

jsmn_parser parser;
jsmntok_t *tokens = RT_NULL;
int tokens_len = 0;
jsmn_item_t root;
jsmn_item_t root_item_data;
jsmn_item_t root_item_sn;
jsmn_item_t root_item_id;
jsmn_item_t root_item_tsl;
char *sn = RT_NULL, *tsl = RT_NULL, *msg_id = RT_NULL;

jsmn_init(&parser);
tokens_len = jsmn_parse(&parser, payload, payloadlen, tokens, JSMNTOK_NUM_SHORT);
if(tokens_len <= 0) {
    LOG_E("jsmn_parse failed!, err=%d, JSMNTOK_NUM_SHORT=%d", tokens_len, JSMNTOK_NUM_SHORT); 
    goto _exit_free;
} 
LOG_D("tokens_len=%d, JSMNTOK_NUM_SHORT=%d", tokens_len, JSMNTOK_NUM_SHORT);

JSMN_ItemInit(&root, tokens, 0, tokens_len);

/* "sn": */
if(JSMN_GetObjectItem(payload, &root, "sn", &root_item_sn) != 0) {
    LOG_E("sn object parse failed!"); 
    goto _exit_free;
}
sn = JSMN_GetValueString(payload, &root_item_sn);

/* "tsl": */
if(JSMN_GetObjectItem(payload, &root, "tsl", &root_item_tsl) != 0) {
    LOG_E("tsl object parse failed!"); 
    goto _exit_free;
}
tsl = JSMN_GetValueString(payload, &root_item_tsl);

/* "id": */
if(JSMN_GetObjectItem(payload, &root, "id", &root_item_id) != 0) {
    LOG_E("id object parse failed!"); 
    goto _exit_free;
}
msg_id = JSMN_GetValueString(payload, &root_item_id);

/* "data":{} */
if(JSMN_GetObjectItem(payload, &root, "data", &root_item_data) != 0) {
    LOG_E("data object parse failed!"); 
    goto _exit_free;
}

3.5、jsmn使用注意事项

  • jsmn没有提供json组包的功能
  • jsmn解析过程中不会动态的申请内存,但是需要预先定义足够大小的 token 数组
  • JSMN_GetString 和 JSMN_GetValueString 会破坏原始数据的格式,会将后"改为\0,跟strtok类似,也就是通过这2个函数获取完字符类型的key或value后,原始json数据会被截断,再调用strlen等函数会有问题。这种情况可以调用JSMN_GetStringBuffered 和 JSMN_GetValueStringBuffered 将key或value存储在传入的buf中
char *JSMN_GetString(char *js, jsmn_item_t *item)
{
    if((js == NULL) || (item == NULL))
        return NULL;
    
    if((item->t->type != JSMN_STRING) && (item->t->type != JSMN_PRIMITIVE))
        return NULL;
    
    js[item->t->end] = '\0'; //这里会将后"改为\0
    return (js + item->t->start);
}

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值