4. 文本序列化:JSON格式简介、cJSON库的安装与使用、JSON序列化与反序列化
好,咱们今天聊聊文本序列化里的重头戏——JSON。说实话,在嵌入式世界里,JSON 已经快成“通用语言”了。我最早接触 JSON 是在一个物联网项目里,设备上报数据给云端,服务器那边直接说“你就给我发 JSON 吧”。那时候我还用拼字符串的方式硬凑,后来才发现,有现成的库不用,纯属给自己找麻烦。
4.1 JSON 格式简介
JSON,全称 JavaScript Object Notation,但别被名字骗了——它跟 JavaScript 没半毛钱关系。说白了,JSON 就是一种轻量级的文本数据交换格式。它长什么样?你肯定见过:
{
"name": "温度传感器",
"id": 1001,
"temperature": 25.6,
"online": true,
"tags": ["工业", "无线"]
}
嗯,看着眼熟吧?JSON 的核心结构就两种:
- 对象(Object):用花括号
{}包裹,里面是键值对。键必须是字符串,值可以是任意类型。 - 数组(Array):用方括号
[]包裹,里面是一组有序的值。
值类型包括:字符串、数字、布尔值、null、对象、数组。就这么简单。我在项目中遇到过有人把 JSON 和 XML 对比,说 XML 太啰嗦。确实,JSON 的语法更紧凑,解析也更快。对于嵌入式设备那点可怜的 RAM 来说,JSON 简直是福音。
4.2 cJSON 库的安装与使用
手动解析 JSON?别傻了。C 语言里最流行的 JSON 库就是 cJSON。它轻量、开源、纯 C 实现,整个库就两个文件:cJSON.c 和 cJSON.h。我习惯直接把它扔进项目源码里,编译时一起编进去。
4.2.1 安装方式
有两种方式:
- 源码拷贝:从 GitHub 下载,把 cJSON.c 和 cJSON.h 放到你的项目目录,直接 #include 就能用。
- 包管理器安装:如果你用 Linux,可以
sudo apt install libcjson-dev。编译时链接-lcjson。
我个人推荐第一种。为什么?嵌入式环境千奇百怪,包管理器不一定有。源码拷贝最可控。我曾经在一个 RTOS 项目里用包管理器装,结果版本不对,折腾了半天。后来老老实实拷贝源码,一分钟搞定。
4.2.2 核心 API 速览
cJSON 的 API 设计得很直观。我列几个最常用的:
| 函数 | 作用 |
|---|---|
cJSON_Parse() |
把 JSON 字符串解析成 cJSON 对象树 |
cJSON_Print() |
把 cJSON 对象树格式化成字符串 |
cJSON_GetObjectItem() |
从对象中获取指定键的值 |
cJSON_CreateObject() |
创建一个空的 JSON 对象 |
cJSON_AddStringToObject() |
向对象添加字符串字段 |
cJSON_Delete() |
释放 cJSON 对象树的内存 |
你想想看,就这几个函数,基本覆盖了 90% 的使用场景。别被吓到,其实很简单。
4.3 JSON 序列化与反序列化
好,重头戏来了。序列化就是把 C 语言的结构体变成 JSON 字符串;反序列化就是反过来,把 JSON 字符串解析回结构体。说白了,就是让数据在“内存结构”和“文本格式”之间来回倒腾。
4.3.1 反序列化:从 JSON 字符串到结构体
假设我们有这样一个结构体:
typedef struct {
char name[32];
int id;
float temperature;
int online;
} SensorData;
现在收到一段 JSON:{"name":"温度传感器","id":1001,"temperature":25.6,"online":true}。怎么把它填进结构体?看代码:
#include <stdio.h>
#include <string.h>
#include "cJSON.h"
int parse_sensor_data(const char *json_str, SensorData *data) {
cJSON *root = cJSON_Parse(json_str);
if (root == NULL) {
printf("JSON 解析失败\n");
return -1;
}
cJSON *item = cJSON_GetObjectItem(root, "name");
if (cJSON_IsString(item)) {
strncpy(data->name, item->valuestring, sizeof(data->name) - 1);
}
item = cJSON_GetObjectItem(root, "id");
if (cJSON_IsNumber(item)) {
data->id = item->valueint;
}
item = cJSON_GetObjectItem(root, "temperature");
if (cJSON_IsNumber(item)) {
data->temperature = item->valuedouble;
}
item = cJSON_GetObjectItem(root, "online");
if (cJSON_IsBool(item)) {
data->online = item->valueint;
}
cJSON_Delete(root);
return 0;
}
注意几个坑:
- 每次取完值要检查类型,
cJSON_IsString()、cJSON_IsNumber()这些判断不能省。我曾经偷懒没检查,结果收到一个空字符串,程序直接崩了。 - 字符串拷贝要用
strncpy,防止缓冲区溢出。 - 用完记得
cJSON_Delete(),否则内存泄漏。嵌入式环境内存金贵,别不当回事。
4.3.2 序列化:从结构体到 JSON 字符串
反过来,把结构体变成 JSON 字符串:
char *serialize_sensor_data(const SensorData *data) {
cJSON *root = cJSON_CreateObject();
if (root == NULL) return NULL;
cJSON_AddStringToObject(root, "name", data->name);
cJSON_AddNumberToObject(root, "id", data->id);
cJSON_AddNumberToObject(root, "temperature", data->temperature);
cJSON_AddBoolToObject(root, "online", data->online);
char *json_str = cJSON_Print(root);
cJSON_Delete(root);
return json_str; // 调用者负责 free()
}
这里有个细节:cJSON_Print() 返回的字符串是 malloc 出来的,调用者用完必须 free()。我见过有人忘了 free,结果内存一点点被吃光。嗯,调试这种问题最头疼了。
cJSON_Create* + cJSON_Print,反序列化用 cJSON_Parse + cJSON_GetObjectItem。记住这个套路,JSON 处理就通了。
4.4 知识体系总览
为了让你看得更清楚,我画了一张图,把本章的知识结构串起来:
好了,JSON 序列化这块就聊到这儿。cJSON 库虽然小,但五脏俱全。你只要掌握了 Parse 和 Print 这对核心操作,再配合 GetObjectItem 和 Add*ToObject,基本就能应对 90% 的 JSON 处理场景。剩下的,遇到具体需求再查文档也不迟。