13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析RapidJSON 是腾讯的开源 JSON 解析框架 是一个高效的 C JSON 解析 生成器 凭借其简洁的 API 和优化的内存管理 它成为了许多开发者的首选 JSON 库 本文从性能 优点 用法 内部结构及注意事项等五个方面对其进行详解

大家好,欢迎来到IT知识分享网。

RapidJSON是腾讯的开源JSON解析框架,是一个高效的C++ JSON解析/生成器。凭借其简洁的API和优化的内存管理,它成为了许多开发者的首选JSON库。本文从性能、优点、用法、内部结构及注意事项等五个方面对其进行详解。

一 RapidJSON性能对比

RapidJSON和其他常见C++ JSON库的解析耗时对比如下:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

可见RapidJson在一众C++ Json库中,性能非常优秀!解析耗时是JsonCpp的1/20、CAJUN的1/141

二 RapidJSON优点

RapidJSON 是腾讯的开源JSON解析框架,是一个高效的C++ JSON解析/生成器,提供SAX及DOM风格API,它的灵感来自RapidXml,有如下优点:

  • RapidJSON 小而全。它同时支持 SAX 和 DOM 风格的 API。SAX 解析器只有约 500 行代码。
  • RapidJSON 快。它的性能可与 strlen() 相比。可支持 SSE2/SSE4.2 加速。
  • RapidJSON 独立。它不依赖于 BOOST 等外部库。它甚至不依赖于 STL。
  • RapidJSON 对内存友好。在大部分 32/64 位机器上,每个 JSON 值只占 16 字节(除字符串外)。它预设使用一个快速的内存分配器,令分析器可以紧凑地分配内存。
  • RapidJSON 对 Unicode 友好。它支持 UTF-8、UTF-16、UTF-32 (大端序/小端序),并内部支持这些编码的检测、校验及转码。例如,RapidJSON 可以在分析一个 UTF-8 文件至 DOM 时,把当中的 JSON 字符串转码至 UTF-16。它也支持代理对(surrogate pair)及 “\u0000″(空字符)。

三 RapidJSON用法

3.1 查询Value

假设我们用 C 语言的字符串储存一个 JSON:

{ "hello": "world", "t": true , "f": false, "n": null, "i": 123, "pi": 3.1416, "a": [1, 2, 3, 4] } 

把它解析至一个 Document:

#include "rapidjson/document.h" using namespace rapidjson; // ... Document document; document.Parse(json);

那么现在该 JSON 就会被解析至 document 中,成为一棵 *DOM 树 *:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

查询一下根 Object 中有没有 “hello” 成员。由于一个 Value 可包含不同类型的值,我们可能需要验证它的类型,并使用合适的 API 去获取其值。在此例中,”hello” 成员关联到一个 JSON String。

assert(document.HasMember("hello")); assert(document["hello"].IsString()); printf("hello = %s\n", document["hello"].GetString());

3.2 查询Array

缺省情况下,SizeType 是 unsigned 的 typedef。在多数系统中,Array 最多能存储 2^32-1 个元素。

你可以用整数字面量访问元素,如 a[0]、a[1]、a[2]。

Array 与 std::vector 相似,除了使用索引,也可使用迭代器来访问所有元素。

for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr) printf("%d ", itr->GetInt());

3.3 查询 Object

和 Array 相似,我们可以用迭代器去访问所有 Object 成员:

static const char* kTypeNames[] = { "Null", "False", "True", "Object", "Array", "String", "Number" }; for (Value::ConstMemberIterator itr = document.MemberBegin(); itr != document.MemberEnd(); ++itr) { printf("Type of member %s is %s\n", itr->name.GetString(), kTypeNames[itr->value.GetType()]); }

3.4 查询 Number

JSON 只提供一种数值类型──Number。数字可以是整数或实数。RFC 4627 规定数字的范围由解析器指定。

由于 C++ 提供多种整数及浮点数类型,DOM 尝试尽量提供最广的范围及良好性能。

当解析一个 Number 时, 它会被存储在 DOM 之中,成为下列其中一个类型:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

当查询一个 Number 时, 你可以检查该数字是否能以目标类型来提取:

3.5 查询String

除了 GetString(),Value 类也有一个 GetStringLength()。这里会解释个中原因。

根据 RFC 4627,JSON String 可包含 Unicode 字符 U+0000,在 JSON 中会表示为 “\u0000″。问题是,C/C++ 通常使用空字符结尾字符串(null-terminated string),这种字符串把 `‘\0’` 作为结束符号。

为了符合 RFC 4627,RapidJSON 支持包含 U+0000 的 String。若你需要处理这些 String,便可使用 GetStringLength() 去获得正确的字符串长度。

例如,当解析以下的 JSON 至 Document d 之后:

{ "s" : "a\u0000b" }

“a\u0000b” 值的正确长度应该是 3。但 strlen() 会返回 1。

GetStringLength() 也可以提高性能,因为用户可能需要调用 strlen() 去分配缓冲。

此外,std::string 也支持这个构造函数:

string(constchar* s, size_t count);

此构造函数接受字符串长度作为参数。它支持在字符串中存储空字符,也应该会有更好的性能。

3.6 比较两个 Value

你可使用 == 及 != 去比较两个 Value。当且仅当两个 Value 的类型及内容相同,它们才当作相等。你也可以比较 Value 和它的原生类型值。以下是一个例子。

if (document["hello"] == document["n"]) /*...*/; // 比较两个值 if (document["hello"] == "world") /*...*/; // 与字符串字面量作比较 if (document["i"] != 123) /*...*/; // 与整数作比较 if (document["pi"] != 3.14) /*...*/; // 与 double 作比较

3.7 改变 Value 类型

当使用默认构造函数创建一个 Value 或 Document,它的类型便会是 Null。要改变其类型,需调用 SetXXX() 或赋值操作,例如:

Document d; // Null d.SetObject(); Value v; // Null v.SetInt(10); v = 10; // 简写,和上面的相同

3.8 转移语义

在设计 RapidJSON 时有一个非常特别的决定,就是 Value 赋值并不是把来源 Value 复制至目的 Value,而是把来源 Value 转移(move)至目的 Value。例如:

Value a(123); Value b(456); b = a; // a 变成 Null,b 变成数字 123。
13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

这么做的好处是:减少了大量的内存拷贝

3.9 创建 String

RapidJSON 提供两个 String 的存储策略。

  1. copy-string: 分配缓冲区,然后把来源数据复制至它。
  2. const-string: 简单地储存字符串的指针。

Copy-string 总是安全的,因为它拥有数据的克隆。Const-string 可用于存储字符串字面量。当我们把一个 copy-string 赋值时, 调用含有 allocator 的 SetString() 重载函数:

Document document; Value author; char buffer[10]; int len = sprintf(buffer, "%s %s", "Milo", "Yip"); // 动态创建的字符串。 author.SetString(buffer, len, document.GetAllocator()); memset(buffer, 0, sizeof(buffer)); // 清空 buffer 后 author.GetString() 仍然包含 "Milo Yip"

正常解析:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

原位解析:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

3.10 修改Array

Array 类型的 Value 提供与 std::vector 相似的 API。

  • Clear()
  • Reserve(SizeType, Allocator&)
  • Value& PushBack(Value&, Allocator&)
  • template <typename T> GenericValue& PushBack(T, Allocator&)
  • Value& PopBack()
  • ValueIterator Erase(ConstValueIterator pos)
  • ValueIterator Erase(ConstValueIterator first, ConstValueIterator last)

注意,Reserve(…) 及 PushBack(…) 可能会为数组元素分配内存,所以需要一个 allocator。

以下是 PushBack() 的例子:

Value a(kArrayType); Document::AllocatorType& allocator = document.GetAllocator(); for (int i = 5; i <= 10; i++) a.PushBack(i, allocator); // 可能需要调用 realloc() 所以需要 allocator // 流畅接口(Fluent interface) a.PushBack("Lua", allocator).PushBack("Mio", allocator);

3.11 修改Object

Object 是键值对的集合。每个键必须为 String。要修改 Object,方法是增加或移除成员。以下的 API 用来增加成员:

  • Value& AddMember(Value&, Value&, Allocator& allocator)
  • Value& AddMember(StringRefType, Value&, Allocator&)
  • template <typename T> Value& AddMember(StringRefType, T value, Allocator&)

以下是一个例子。

Value contact(kObject); contact.AddMember("name", "Milo", document.GetAllocator()); contact.AddMember("married", true, document.GetAllocator());

四 RapidJSON内部结构

4.1 GenericValue

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

4.2 JSON数据加载&输出

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

4.3 SAX 和 DOM

下面的 UML 图显示了 SAX 和 DOM 的基本关系。

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

关系的核心是 Handler 概念。在 SAX 一边,Reader 从流解析 JSON 并将事件发送到 Handler。Writer 实现了 Handler 概念,用于处理相同的事件。在 DOM 一边,Document 实现了 Handler 概念,用于通过这些时间来构建 DOM。Value 支持了 Value::Accept(Handler&) 函数,它可以将 DOM 转换为事件进行发送。

在这个设计,SAX 是不依赖于 DOM 的。甚至 Reader 和 Writer 之间也没有依赖。这提供了连接事件发送器和处理器的灵活性。除此之外,Value 也是不依赖于 SAX 的。所以,除了将 DOM 序列化为 JSON 之外,用户也可以将其序列化为 XML,或者做任何其他事情。

4.4 工具类

SAX 和 DOM API 都依赖于3个额外的概念:Allocator、Encoding 和 Stream。它们的继承层次结构如下图所示。

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

4.5 值的数据布局

Value 是可变类型。在 RapidJSON 的上下文中,一个 Value 的实例可以包含6种 JSON 数据类型之一。通过使用 union ,这是可能实现的。每一个 Value 包含两个成员:union Data data_ 和 unsigned flags_。flags_ 表明了 JSON 类型,以及附加的信息。

下表显示了所有类型的数据布局。32位/64位列表明了字段所占用的字节数。

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

4.6 解析器

迭代解析器是一个以非递归方式实现的递归下降的 LL(1) 解析器。

解析器使用的语法是基于严格 JSON 语法的:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

注意到左因子被加入了非终结符的 values 和 members 来保证语法是 LL(1) 的。

在 RapidJSON 中,解析时会将解析表编码为状态机。 规则由头部和主体组成。 状态转换由规则构造。 除此之外,额外的状态被添加到与 array 和 object 有关的规则。 通过这种方式,生成数组值或对象成员可以只用一次状态转移便可完成, 而不需要在直接的实现中的多次出栈/入栈操作。 这也使得估计栈的大小更加容易。

状态图如如下所示:

13.6k star,比JsonCpp快20倍,腾讯RapidJSON的用法及原理剖析

五 RapidJson的注意事项

  • Value赋值使用move语义,而不是copy语义。也就是说,拷贝构造和拷贝赋值函数都是用move语义实现的。
  • 当string的生命周期不足时,Value应该使用Copy-string存储策略,否则value无法长期存储字符串。
  • StringBuffer是一个简单的输出流,当该缓冲区满溢时会自动增加容量(默认为256个字符)。
  • FileReadStream / FileWriteStream,和IStreamWrapper / OStreamWrapper,它们都是字节流,不处理编码。若输入流或输出流数据是非UTF-8编码时,输入流数据需要用EncodedInputStream或AutoUTFInputStream包装,而输出流数据需要用EncodedOutputStream 或AutoUTFOutputStream包装。
  • 原位解析适合用于短期的、用完即弃的JSON。数据流的来源编码与目标编码必须相同,并且还要保证缓冲区的生命周期大于DOM。
  • RapidJSON支持SSE2,SSE4.2和ARM Neon指令加速解析,并且只对UTF-8编码的内存流启用。然而,若在不支持这些指令集的机器上执行这些可执行文件,会导致崩溃
  • 不建议使用wistreamwostream

六 参考资料

[1] github:git@github.com:Tencent/rapidjson.git

[2] rapidjson官网:http://rapidjson.org/zh-cn/index.html

[3] c++ json库对比:https://blog.csdn.net/_/article/details/

[4] rapidjson讲解:https://blog.csdn.net/kds0714/article/details/

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/97950.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信