最近在修已经离职同事的代码,它的代码主要是负责与云平台进行交互的,交互的协议采用的是标准的JSON格式;他用的是cJSON库来进行解析。不幸的是,大佬的防御性编程技能极差,很多地方该进行内存释放的却没有进行内存释放,还有很多地方该进行指针判空也没有进行指针判空,而是直接就拿来用了。在多线程环境里,这样的做法极其危险。因此,在我和另外一位同事接手,出现了大量的白屏、卡死等问题。后来,经过一段时间的定位,发现是程序编写过程中不严谨造成的。
除了以上问题以外,由于我们的嵌入式平台采用的是低成本的ARM芯片,RAM只有32MB,因此使用cJSON库在解析的过程中可能较占用资源;在后期程序重构时可能会考虑使用jsmn来进行云平台数据交互的解析。
本期给大家带来的开源项目是 jsmn,一个资源占用极小的json解析器,号称世界上最快,作者zserge,目前收获 2.9K 个 star,遵循 MIT 开源许可协议。
jsmn主要有以下特性:
项目地址:https://github.com/zserge/jsmn
开源项目在移植过程中主要参考项目的readme文档,一般只需两步:
本文中我使用的是小熊派IoT开发套件,主控芯片为STM32L431RCT6:
移植之前需要准备一份裸机工程,我使用STM32CubeMX生成,需要初始化以下配置:
具体过程可以参考:
① 复制jsmn源码到工程中:
② 将 jsmn.h 文件添加到keil中(没有实质作用,方便编辑):③ 添加jsmn头文件路径:
① 包含jsmn头文件
使用时包含头文件,因为jsmn的函数定义也是在头文件中,所以第一次添加的时候,可以直接添加:
/* USER CODE BEGIN Includes */
#include "jsmn.h"
#include //用于printf打印
#include //用于字符串处理
/* USER CODE END Includes */
已经使用过之后,在别的文件中继续使用时,需要这样添加,且顺序不可互换:
/* USER CODE BEGIN 0 */
#define JSMN_HEADER
#include "jsmn.h"
/* USER CODE END 0 */
否则会造成函数重定义:
② 设置一段原始json数据
在main.c中设置原始的json数据,用于后续解析:
/* USER CODE BEGIN PV */
static const char *JSON_STRING =
"{\"user\": \"johndoe\", \"admin\": false, \"uid\": 1000,\n"
"\"groups\": [\"users\", \"wheel\", \"audio\", \"video\"]}";
/* USER CODE END PV */
③ 开辟一块存放token的数组(token池)
jsmn中,每个数据段解析出来之后是一个token,关于token的详细解释,请参考下文第4.1小节。
/* USER CODE BEGIN PV */
jsmntok_t t[128];
/* USER CODE END PV */
④ 编写在原始JSON数据中的字符串比较函数:
static int jsoneq(const char *json, jsmntok_t *tok, const char *s) {
if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start &&
strncmp(json + tok->start, s, tok->end - tok->start) == 0) {
return 0;
}
return -1;
}
在main函数的开始创建解析器:
/* USER CODE BEGIN 1 */
int r;
int i;
jsmn_parser p;//jsmn解析器
/* USER CODE END 1 */
在随后外设初始化完成之后的代码中初始化解析器:
/* USER CODE BEGIN 2 */
jsmn_init(&p);
/* USER CODE END 2 */
r = jsmn_parse(&p, JSON_STRING, strlen(JSON_STRING), t,sizeof(t) / sizeof(t[0]));
if (r < 0) {
printf("Failed to parse JSON: %d\n", r);
return 1;
}
/* Assume the top-level element is an object */
if (r < 1 || t[0].type != JSMN_OBJECT) {
printf("Object expected\n");
return 1;
}
/* Loop over all keys of the root object */
for (i = 1; i < r; i++)
{
if (jsoneq(JSON_STRING, &t[i], "user") == 0)
{
/* We may use strndup() to fetch string value */
printf("- user: %.*s\n", t[i + 1].end - t[i + 1].start,
JSON_STRING + t[i + 1].start);
i++;
}
else if (jsoneq(JSON_STRING, &t[i], "admin") == 0)
{
/* We may additionally check if the value is either "true" or "false" */
printf("- Admin: %.*s\n", t[i + 1].end - t[i + 1].start,
JSON_STRING + t[i + 1].start);
i++;
}
else if (jsoneq(JSON_STRING, &t[i], "uid") == 0)
{
/* We may want to do strtol() here to get numeric value */
printf("- UID: %.*s\n", t[i + 1].end - t[i + 1].start,
JSON_STRING + t[i + 1].start);
i++;
}
else if (jsoneq(JSON_STRING, &t[i], "groups") == 0)
{
int j;
printf("- Groups:\n");
if (t[i + 1].type != JSMN_ARRAY)
{
continue; /* We expect groups to be an array of strings */
}
for (j = 0; j < t[i + 1].size; j++)
{
jsmntok_t *g = &t[i + j + 2];
printf(" * %.*s\n", g->end - g->start, JSON_STRING + g->start);
}
i += t[i + 1].size + 1;
}
else
{
printf("Unexpected key: %.*s\n", t[i].end - t[i].start,
JSON_STRING + t[i].start);
}
}
编译、下载到开发板,使用串口助手进行测试:
jsmn对json数据中的每一个数据段都会抽象为一个结构体,称之为token,此结构体非常简洁:
/**
* 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 jsmntok {
jsmntype_t type;
int start;
int end;
int size;
#ifdef JSMN_PARENT_LINKS
int parent;
#endif
} jsmntok_t;
在本实验中未开启JSMN_PARENT_LINKS
,所以此结构体占用16Byte大小。
从结构体中的数据成员可以看出,jsmn并不保存任何具体的数据内容,仅仅记录:
其中,数据项的类型支持4种:
/**
* 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;
上述说到jsmn将每一个json数据段都抽象为一个token,那么jsmn是如何对整段json数据进行解析,得到每一个数据项的token呢?
jsmn解析器也是非常简洁的一个结构体:
/**
* JSON parser. Contains an array of token blocks available. Also stores
* the string being parsed now and current position in that string.
*/
typedef struct jsmn_parser {
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;
jsmn解析就是将json数据逐个字符进行解析,用pos数据成员来记录解析器当前的位置,当寻找到特殊字符时,就去之前我们定义的token数组(t)中申请一个空的token成员,将该token在数组中的位置记录在数据成员toknext中。
源码在下面的函数中,代码过多,暂且先不放:
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
jsmntok_t *tokens, const unsigned int num_tokens);
下面用一个实例来看看token是怎么分配的。
缩短json原始数据:
static const char *JSON_STRING =
"{\"name\":\"mculover666\",\"admin\":false,\"uid\":1000}";
在解析之后将每个token打印出来:
printf("[type][start][end][size]\n");
for(i = 0;i < r; i++)
{
printf("[%4d][%5d][%3d][%4d]\n", t[i].type, t[i].start, t[i].end, t[i].size);
}
结果如下:这段json数据解析出的token有7个:
① Object类型的token:{\"name\":\"mculover666\",\"admin\":false,\"uid\":1000}
② String类型的token:"name"
、"mculover666"
、"admin"
、"uid"
③ Primitive类型的token:数字1000
,布尔值false
在解析完毕获得这些token之后,需要根据token数量来判断是否解析成功:
① 返回的token数量<0:证明解析失败,返回值代表了错误类型:
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的内容作为该键值对的值。
由于微信公众号近期改变了推送规则,为了防止找不到,可以星标置顶,这样每次推送的文章才会出现在您的订阅列表里。
猜你喜欢:
嵌入式设备AP配网实例分享
嵌入式Linux单板连接飞燕物联网平台
分享一种灵活性很高的协议格式(附代码例子)
嵌入式大杂烩周记 | 第 16 期
嵌入式大杂烩周记 | 第 15 期
访问非法内存为什么不会出错?
嵌入式大杂烩周记 | 第 14 期
分享几个实用的代码片段(第二弹)
分享一种你可能不知道的bug定位方法
分享一种修改配置文件的方法
《嵌入式大杂烩周记第 13 期:lz4》
《嵌入式并行多线程处理器,了解一下!》
《分享一种修改配置文件的方法》
《分享几个实用的代码片段(附代码例子)》
《废旧板子再利用:搭建无线调试环境!》
《嵌入式段错误的3种调试方法汇总!》
《简说TCP通信非阻塞接收(附代码例子)》
《TCP通信常用接口的使用封装》
《嵌入式软件中,总线错误的坑?替大家先踩一步》
《分享嵌入式软件调试方法及几个有用的工具!》
《分享两点提高编程能力的建议!》
在公众号聊天界面回复1024,可获取嵌入式资源;回复 m ,可查看文章汇总