admin管理员组文章数量:1530842
问题描述
在只有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种思路
- 限制json数据的嵌套深度和数据大小,但是如果限制数据量大小还得考虑分包,太简单的结构表达不了这么复杂的数据
- 采用不必展开的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);
}
版权声明:本文标题:JSON的C语言解析库---cJSON与jsmn及其应用 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1725889421a1047380.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论