天天看點

推薦一個資源占用極少的json解析器

作者:嵌入式胖胖

最近在修已經離職同僚的代碼,它的代碼主要是負責與雲平台進行互動的,互動的協定采用的是标準的JSON格式;他用的是cJSON庫來進行解析。不幸的是,大佬的防禦性程式設計技能極差,很多地方該進行記憶體釋放的卻沒有進行記憶體釋放,還有很多地方該進行指針判空也沒有進行指針判空,而是直接就拿來用了。在多線程環境裡,這樣的做法極其危險。是以,在我和另外一位同僚接手,出現了大量的白屏、卡死等問題。後來,經過一段時間的定位,發現是程式編寫過程中不嚴謹造成的。

除了以上問題以外,由于我們的嵌入式平台采用的是低成本的ARM晶片,RAM隻有32MB,是以使用cJSON庫在解析的過程中可能較占用資源;在後期程式重構時可能會考慮使用jsmn來進行雲平台資料互動的解析。

本期給大家帶來的開源項目是 jsmn,一個資源占用極小的json解析器,号稱世界上最快,作者zserge,目前收獲 2.9K 個 star,遵循 MIT 開源許可協定。

推薦一個資源占用極少的json解析器

jsmn主要有以下特性:

  • 沒有任何庫依賴關系;
  • 文法與C89相容,代碼可移植性高;
  • 沒有任何動态記憶體配置設定
  • 極小的代碼占用
  • API隻有兩個,極其簡潔
項目位址:https://github.com/zserge/jsmn

移植jsmn

移植思路

開源項目在移植過程中主要參考項目的readme文檔,一般隻需兩步:

  • ① 添加源碼到裸機工程中;
  • ② 實作需要的接口;

準備裸機工程

本文中我使用的是小熊派IoT開發套件,主要晶片為STM32L431RCT6:

推薦一個資源占用極少的json解析器

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

  • 配置一個序列槽用于發送資料;
  • printf重定向

具體過程可以參考:

  • STM32CubeMX_07 | 使用USART發送和接收資料(中斷模式)
  • STM32CubeMX_09 | 重定向printf函數到序列槽輸出的多種方法

添加jsmn到工程中

① 複制jsmn源碼到工程中:

推薦一個資源占用極少的json解析器

② 将 jsmn.h 檔案添加到keil中(沒有實質作用,友善編輯):

推薦一個資源占用極少的json解析器

③ 添加jsmn頭檔案路徑:

推薦一個資源占用極少的json解析器

嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和内容,導緻工資要不上去!

無償分享大家一個資料包,差不多150多G。裡面學習内容、面經、項目都比較新也比較全!某魚上買估計至少要好幾十。

點選這裡找小助理0元領取:點選文中藍色字型即可領取

推薦一個資源占用極少的json解析器
推薦一個資源占用極少的json解析器

使用jsmn解析json資料

準備工作

① 包含jsmn頭檔案

使用時包含頭檔案,因為jsmn的函數定義也是在頭檔案中,是以第一次添加的時候,可以直接添加:

/* USER CODE BEGIN Includes */
#include "jsmn.h"
#include <stdio.h> //用于printf列印
#include <string.h> //用于字元串處理

/* USER CODE END Includes */
           

已經使用過之後,在别的檔案中繼續使用時,需要這樣添加,且順序不可互換:

/* USER CODE BEGIN 0 */
#define JSMN_HEADER
#include "jsmn.h" 

/* USER CODE END 0 */
           

否則會造成函數重定義:

推薦一個資源占用極少的json解析器

② 設定一段原始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 < 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;
  }
           

3.4. 逐個解析token

/* 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);
    }
  }
           

解析結果

編譯、下載下傳到開發闆,使用序列槽助手進行測試:

推薦一個資源占用極少的json解析器

記憶體對比

推薦一個資源占用極少的json解析器

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

結果如下:

推薦一個資源占用極少的json解析器

這段json資料解析出的token有7個:

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

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

③ Primitive類型的token:數字1000,布爾值false

推薦一個資源占用極少的json解析器

使用者如何從token中提取值

在解析完畢獲得這些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的内容作為該鍵值對的值。

原文連結:https://mp.weixin.qq.com/s/EyYO48JB4jLOO0xU_dVqEw

轉載自:嵌入式大雜燴

原文連結:推薦一個資源占用極少的json解析器!

本文來源網絡,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯系我進行删除。