一、简介
一直使用cJSON
来操作Json
,感觉接口使用没那么方便。因为之前用过libconfig
这个库,这个库使用很方便,于是萌生了封装一下的念头,封装成使用习惯和libconfig
类似的。
封装过程不说了,增加了3个类Tree、Node、CException
,都放到命名空间JsonTree
中。
二、封装后使用方法
封装后可以很方便的操作增删改查了!
直接上使用的代码:
/* JsonTree解析Json,修改后更新Json
* 使用下面的Json字符串测试
{
"node1": {
"key1": null,
"key2": "string1",
"key3": [12, 34, 56, 78],
"key4": [123, 456, 789],
"sub1": {
"key1": 94,
"key2": "string2",
"key3": [99, 88, 77, 66, 55, 44],
"key4": [{
"aaa": 111,
"aaa2": "cow",
"aaa3": ["a", "b", "c"]
}, {
"bbb": 222
}, {
"ccc": 333
}, {
"ddd": 444
}],
"sub1": {
"key5": "Hello"
}
}
}
}
*/
using namespace JsonTree;
static void JsonTreeDemo()
{
std::string str = "{\n"
" \"node1\": {\n"
" \"key1\": null,\n"
" \"key2\": \"string1\",\n"
" \"key3\": [12, 34, 56, 78],\n"
" \"key4\": [123, 456, 789],\n"
" \"sub1\": {\n"
" \"key1\": 94,\n"
" \"key2\": \"string2\",\n"
" \"key3\": [99, 88, 77, 66, 55, 44],\n"
" \"key4\": [{\n"
" \"aaa\": 111,\n"
" \"aaa2\": \"cow\",\n"
" \"aaa3\": [\"a\", \"b\", \"c\"]\n"
" }, {\n"
" \"bbb\": 222\n"
" }, {\n"
" \"ccc\": 333\n"
" }, {\n"
" \"ddd\": 444\n"
" }],\n"
" \"sub1\": {\n"
" \"key5\": \"Hello\"\n"
" }\n"
" }\n"
" }\n"
"}";
try {
// 通过Json转成Tree,可以方便的读取每项的值
Tree tree(str);
if (tree.IsValid())
{
Node& root = tree.Root();
// 读取node1的keys,值是string1
std::string strVal1 = root["node1"]["key2"]; // string1
// 读取node1的sub1里面的key2
std::string strVal2 = root["node1"]["sub1"]["key2"]; // string2
// 数组访问
// 比如读取node1->sub1->key4->aaa3, key4是数组, aaa3也是数组, aaa3是在数组key4的[0]中的,注意写法
// 这个写法错误,会引发异常“NodeNoExistException: 'root.node1.sub1.key4.aaa3' does not exist!”
// std::string strVal3 = root["node1"]["sub1"]["key4"]["aaa3"][0];
// 正确写法
std::string strVal3 = root["node1"]["sub1"]["key4"][0]["aaa3"][0]; // "a"
std::string strVal4 = root["node1"]["sub1"]["key4"][0]["aaa3"][1]; // "b"
std::string strVal5 = root["node1"]["sub1"]["key4"][0]["aaa3"][2]; // "c"
// 支持数组长度
size_t size = root["node1"]["sub1"]["key4"][0]["aaa3"].Lengeh(); // 3
// 读取整个数组,比如:node1->key3
size = root["node1"]["key3"].Lengeh(); // 先读取长度
// 读取数组所有值
for (size_t i = 0; i < size; ++i)
{
int nVal = root["node1"]["key3"][i]; // 分别是12,34,56,78
std::cout << nVal << std::endl;
}
// Tree可以转换回Json对像,再使用cJSON_Print转回字符串
// cJSON_Parse解析str, 再使用cJSON_Print转回字符串
// 比较这两个字符串是相等的,确认Tree转回的Json也是正确的
cJSON* refJson = tree.ToCJson();
cJSON* json_root = cJSON_Parse(str.c_str());
if (strcmp(cJSON_Print(refJson), cJSON_Print(json_root)) == 0)
{
std::cout << "Test Tree OK" << std::endl;
}
else
{
std::cout << "Test Tree error" << std::endl;
}
cJSON_Delete(json_root);
// Tree可以删除某个项,比如删除node1->sub1->key3
// 两种写法
// 写法1
Node& node1 = root["node1"]["sub1"];
node1.remove_son("key3");
// 写法2,比如删除node1->sub1->key2
root["node1"]["sub1"]["key2"].remove();
// 删除数组的某项,比如node1->key4是数组[123, 456, 789], 删除数组下标是[1]的456
root["node1"]["key4"].remove_son(1);
// 修改值,比如node1->sub1->key1值94,改成字符串值"I am a test"
root["node1"]["sub1"]["key1"] = "I am a test";
// 修改数组值,比如node1->key4是数组,上面删除456之后,是[123, 789],把123改成987
root["node1"]["key4"][0] = 987;
// 可以增加项,比如增加node1->here, 值是okok
root["node1"].add_son("here");
root["node1"]["here"] = "okok";
// 打印出来看看结果是不是和希望的一样,打印还是转回Json,使用cJSON_Print转字符串打印
refJson = tree.ToCJson();
std::cout << "**************** new Json string *****************" << std::endl;
std::cout << cJSON_Print(refJson) << std::endl;
//printf("%s\n", cJSON_Print(refJson));
// 析构函数会自动释放资源,这里的Tree是局部变量,函数返回后会自动析构
}
}
catch (std::exception& e) { // 可以捕捉异常
std::cerr << e.what() << std::endl;
}
}
最后结果:
12
34
56
78
Test Tree OK
**************** new Json string *****************
{
"node1": {
"key1": null,
"key2": "string1",
"key3": [12, 34, 56, 78],
"key4": [987, 789],
"sub1": {
"key1": "I am a test",
"key4": [{
"aaa": 111,
"aaa2": "cow",
"aaa3": ["a", "b", "c"]
}, {
"bbb": 222
}, {
"ccc": 333
}, {
"ddd": 444
}],
"sub1": {
"key5": "Hello"
}
},
"here": "okok"
}
}
从例子中发现,接口使用比原来使用简单容易了,使用上接近libconfig
。
这个简单封装用法,在实际项目中使用了一段比较长的时间了(本文是vs2019
使用写的,但是在海思的ARM平台使用较长时间了,也能用于Linux平台的),常见的Json
暂未发现问题。
三、速度对比
后来,因为要在服务器中使用Json
,因为服务器高并发,需要考虑速度因素,几经查找,看到号称速度很快的RapidJson
,然后了解了一下接口的用法,还是觉得接口比较难使用,和cJSON
差不多,而且有人说某些情况下会有内存泄漏,所以放弃了。
之后查到有[JSON for Modern C++,nlohmann写的json,这个接口非常的好用,是用过的最好用的,用法上和自己封装之后的用法类似,但是是大牛写的,肯定更符合标准化,使用上更放心,于是打算测试一下cJSON、JSON for Modern C++、自己封装的JsonTree
三者的速度,可以肯定Json
在cJSON
上封装的,速度肯定是慢于cJSON
,如果JSON for Modern C++
速度优于或者接近cJSON
,那就可以放心使用了。
测试方式是跑10万个Json
字符串解析,读取出里面每个key的值,测试执行时间,因为仿造10万个Json
比较困难,只是测试速度,所以用一个统一的Json
字符串,按照每次Json
都不同的流程那样的执行来测试。
测试代码如下:
/*
* 以下Json,解释10万次,分别测试测试cJSON、JsonTree、Json for Modern C++所用时间
{
"name": "server",
"id": 123456,
"sql_ip": "192.168.1.100",
"database": "shops",
"user": "root",
"pwd": "user@test.com",
"param1": 111,
"param2": 222,
"param3": "333",
"array": [{
"ip": "192.168.1.200",
"port": 7000
}, {
"ip": "192.168.1.200",
"port": 7000
}, {
"ip": "192.168.1.200",
"port": 7000
}]
}
*/
typedef struct
{
std::string ip;
int port;
}Network;
// 测试cJSON的时间
static void TestcJSON()
{
clock_t start_time = clock();
for (int i = 0; i < 100000; ++i) // 10万次
{
std::string str = "{\n"
" \"name\": \"server\",\n"
" \"id\": 123456,\n"
" \"sql_ip\": \"192.168.1.100\",\n"
" \"database\": \"shops\",\n"
" \"user\": \"root\",\n"
" \"pwd\": \"user@test.com\",\n"
" \"param1\": 111,\n"
" \"param2\": 222,\n"
" \"param3\": \"333\",\n"
" \"array\": [{\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7000\n"
" }, {\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7001\n"
" }, {\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7002\n"
" }]\n"
"}";
std::string name;
int id;
std::string sql_ip;
std::string database;
std::string user;
std::string pwd;
int param1 = 0;
int param2 = 0;
std::string param3;
std::vector<Network> vecNetwork;
cJSON * json_root = cJSON_Parse(str.c_str());
if (json_root != NULL)
{
name = cJSON_GetObjectItem(json_root, "name")->valuestring;
id = cJSON_GetObjectItem(json_root, "id")->valueint;
sql_ip = cJSON_GetObjectItem(json_root, "sql_ip")->valuestring;
database = cJSON_GetObjectItem(json_root, "database")->valuestring;
user = cJSON_GetObjectItem(json_root, "user")->valuestring;
pwd = cJSON_GetObjectItem(json_root, "pwd")->valuestring;
param1 = cJSON_GetObjectItem(json_root, "param1")->valueint;
param2 = cJSON_GetObjectItem(json_root, "param2")->valueint;
param3 = cJSON_GetObjectItem(json_root, "param3")->valuestring;
cJSON* json_arry = cJSON_GetObjectItem(json_root, "array");
if (json_arry != NULL)
{
cJSON* json_child = json_arry->child;
while (json_child != NULL) {
std::string strip = cJSON_GetObjectItem(json_child, "ip")->valuestring;
int port = cJSON_GetObjectItem(json_child, "port")->valueint;
vecNetwork.push_back({ strip, port });
json_child = json_child->next;
}
}
}
cJSON_Delete(json_root);
}
clock_t end_time = clock();
std::cout << "cJSON Running time is: " << static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000 << "ms" << std::endl;
}
// 测试JsonTree的时间
using namespace JsonTree;
static void TestJsonTree()
{
clock_t start_time = clock();
for (int i = 0; i < 100000; ++i)
{
std::string str = "{\n"
" \"name\": \"server\",\n"
" \"id\": 123456,\n"
" \"sql_ip\": \"192.168.1.100\",\n"
" \"database\": \"shops\",\n"
" \"user\": \"root\",\n"
" \"pwd\": \"user@test.com\",\n"
" \"param1\": 111,\n"
" \"param2\": 222,\n"
" \"param3\": \"333\",\n"
" \"array\": [{\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7000\n"
" }, {\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7001\n"
" }, {\n"
" \"ip\": \"192.168.1.200\",\n"
" \"port\": 7002\n"
" }]\n"
"}";
JsonTree::Tree tree(str);
if (tree.IsValid())
{
JsonTree::Node& root = tree.Root();
std::string name = root["name"];
int id = root["id"];
std::string sql_ip = root["sql_ip"];
std::string database = root["database"];
std::string user = root["user"];
std::string pwd = root["pwd"];
int param1 = root["param1"];
int param2 = root["param2"];
std::string param3 = root["param3"];
size_t len = root["array"].Lengeh();
std::vector<Network> vecNetwork;
for (size_t j = 0; j < len; ++j)
{
std::string strip = root["array"][j]["ip"];
int port = root["array"][j]["port"];
vecNetwork.push_back({ strip, port });
}
}
}
clock_t end_time = clock();
std::cout << "JsonTree Running time is: " << static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000 << "ms" << std::endl;
}
// 测试nlohmann Json 的时间
#include "nlohmann/json.hpp" // 要使用nlohmann json 必须把文件夹nlohmann放到工程,以及包含里面的头文件json.hpp
using json = nlohmann::json;
static void TestNlohmannJson()
{
clock_t start_time = clock();
for (int i = 0; i < 100000; ++i)
{
json root = R"(
{
"name": "server",
"id": 123456,
"sql_ip": "192.168.1.100",
"database": "shops",
"user": "root",
"pwd": "user@test.com",
"param1": 111,
"param2": 222,
"param3": "333",
"array": [{
"ip": "192.168.1.200",
"port": 7000
}, {
"ip": "192.168.1.200",
"port": 7001
}, {
"ip": "192.168.1.200",
"port": 7002
}]
}
)"_json;
std::string name = root["name"];
int id = root["id"];
std::string sql_ip = root["sql_ip"];
std::string database = root["database"];
std::string user = root["user"];
std::string pwd = root["pwd"];
int param1 = root["param1"];
int param2 = root["param2"];
std::string param3 = root["param3"];
size_t len = root["array"].size();
std::vector<Network> vecNetwork;
for (size_t j = 0; j < len; ++j)
{
std::string strip = root["array"][j]["ip"];
int port = root["array"][j]["port"];
vecNetwork.push_back({ strip, port });
}
}
clock_t end_time = clock();
std::cout << "NlohmannJson Running time is: " << static_cast<double>(end_time - start_time) / CLOCKS_PER_SEC * 1000 << "ms" << std::endl;
}
static void TestSpeed()
{
TestcJSON();
TestJsonTree();
TestNlohmannJson();
/* 为了表示测试和调用顺序无关,顺序反过来再测试1次 */
TestNlohmannJson();
TestJsonTree();
TestcJSON();
}
测试两次,第2次把测试顺序反过来。每个测试都是10万次。
结果是:
cJSON Running time is: 4317ms
JsonTree Running time is: 29087ms
NlohmannJson Running time is: 54067ms
NlohmannJson Running time is: 54109ms
JsonTree Running time is: 29295ms
cJSON Running time is: 4008ms
可见JSON for Modern C++
的接口使用最好,JsonTree
用法和它比较相似。
然而,封装的代价是巨大的,cJSON
只用4秒多点,封装之后29秒多点,时间多了约7倍,JSON for Modern C++
最慢,54秒多点,是cJSON
的12倍多,是JsonTree
的1.8倍左右。
高并发服务器,最后我只能选择cJSON
,毕竟速度还是最重要的。
如果哪位程序员朋友有又好用又快速的程序,请给我介绍。最后附上整个源程序项目供有兴趣的朋友下载。
四、源码下载
源码下载,VS2019
编译测试通过。