良许Linux教程网 干货合集 推荐一个资源占用极少的json解析器!

推荐一个资源占用极少的json解析器!

最近我在处理一位离职同事编写的代码,他的代码主要负责与云平台进行交互,并且使用标准的JSON格式进行通信。他采用了cJSON库来解析JSON数据。不幸的是,这位同事在防御性编程方面技术很差,很多地方没有进行必要的内存释放操作,也没有进行指针判空,直接使用了指针。在多线程环境中,这种做法非常危险。因此,当我和另一位同事接手这部分代码后,出现了大量的白屏和卡死等问题。经过一段时间的调试和定位,我们发现这些问题是由于代码编写不严谨所导致的。

除了上述问题,由于我们的嵌入式平台使用的是低成本的ARM芯片,RAM只有32MB,因此在使用cJSON库解析数据时可能会占用较多的系统资源。在程序重构阶段,我们可能会考虑使用jsmn来解析云平台的数据交互,因为它的资源占用非常小。

在此,我向大家推荐一个开源项目,称为jsmn。它是一个资源占用极小的JSON解析器,并被称为世界上最快的解析器。它的作者是zserge,目前已经获得了2.9K个星标,并采用了MIT开源许可协议。

image-20230901201544588
image-20230901201544588

jsmn主要有以下特性:

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

项目地址:https://github.com/zserge/jsmn

移植jsmn

移植思路

开源项目在移植过程中主要参考项目的readme文档,一般只需两步:

  • ① 添加源码到裸机工程中;
  • ② 实现需要的接口;

准备裸机工程

本文中我使用的是小熊派IoT开发套件,主控芯片为STM32L431RCT6:image-20230901201548389

移植之前需要准备一份裸机工程,我使用STM32CubeMX生成,需要初始化以下配置:

  • 配置一个串口用于发送数据;
  • printf重定向

具体过程可以参考:

添加jsmn到工程中

① 复制jsmn源码到工程中:image-20230901201551546

② 将 jsmn.h 文件添加到keil中(没有实质作用,方便编辑):

③ 添加jsmn头文件路径:

image-20230901201603758
image-20230901201603758

使用jsmn解析json数据

准备工作

① 包含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 */

否则会造成函数重定义:image-20230901201607192

② 设置一段原始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 */

解析数据,获取token

r = jsmn_parse(&p, JSON_STRING, strlen(JSON_STRING), t,sizeof(t) / sizeof(t[0]));

  if (r printf("Failed to parse JSON: %d\n", r);
    return 1;
  }
  
  /* Assume the top-level element is an object */
  if (r type != JSMN_OBJECT) {
    printf("Object expected\n");
    return 1;
  }

3.4. 逐个解析token

/* Loop over all keys of the root object */
 for (i = 1; 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 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);
    }
  }

解析结果

编译、下载到开发板,使用串口助手进行测试:

image-20230901201616568
image-20230901201616568

内存对比

image-20230901201619671
image-20230901201619671

jsmn设计思想解读

jsmn对json数据项的抽象

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并不保存任何具体的数据内容,仅仅记录:

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

其中,数据项的类型支持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如何解析出每个token

上述说到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 printf("[%4d][%5d][%3d][%4d]\n", t[i].type, t[i].start, t[i].end, t[i].size);
}

结果如下:image-20230901201627172

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

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

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

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

image-20230901201630651
image-20230901201630651

用户如何从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的内容作为该键值对的值。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部