2

我正在使用JsonCpp在 C++ 中解析 JSON。

例如

Json::Reader r;
std::stringstream ss;
ss << "{\"name\": \"sample\"}";

Json::Value v;
assert(r.parse(ss, v));         // OK
assert(v["name"] == "sample");  // OK

但我的实际输入是整个JSON 消息,可能以任意大小的块形式到达;我所能做的就是让 JsonCpp 尝试逐个字符地解析我的输入,当我们发现它们时吃掉完整的 JSON 消息:

Json::Reader r;
std::string input = "{\"name\": \"sample\"}{\"name\": \"aardvark\"}";

for (size_t cursor = 0; cursor < input.size(); cursor++) {  
    std::stringstream ss;
    ss << input.substr(0, cursor);

    Json::Value v;
    if (r.parse(ss, v)) {
        std::cout << v["name"] << " ";
        input.erase(0, cursor);
    }
} // Output: sample aardvark

这已经有点恶心了,但它确实变得更糟了。当输入的一部分丢失(出于任何原因)时,我还需要能够重新同步。

现在它不必是无损的,但我想防止如下输入可能永远破坏解析器:

{"name": "samp{"name": "aardvark"}

将此输入传递给 JsonCpp 将失败,但随着我们在缓冲区中接收到更多字符,该问题不会消失;那第二个在它之前name的那个之后直接无效;"缓冲区永远无法完成以呈现有效的 JSON。

但是,如果我可以被告知片段从第二个n字符开始肯定变得无效,我可以将缓冲区中的所有内容删除到该点,然后等待下一个{考虑新对象的开始,这是最好的-努力重新同步。


那么,有没有一种方法可以让 JsonCpp 告诉我一个不完整的 JSON 片段是否已经保证完整的“对象”在语法上是无效的?

那是:

{"name": "sample"}   Valid        (Json::Reader::parse == true)
{"name": "sam        Incomplete   (Json::Reader::parse == false)
{"name": "sam"LOL    Invalid      (Json::Reader::parse == false)

我想区分这两种失败状态。

我可以使用 JsonCpp 来实现这一点,还是我必须通过构建一个状态机来编写自己的 JSON“部分验证器”,该状态机在输入字符串的每一步都考虑哪些字符是“有效的”?我宁愿不重新发明轮子...

4

3 回答 3

3

这当然取决于您是否真正控制数据包(以及生产者)。如果这样做,最简单的方法是在标题中指示边界:

+---+---+---+---+-----------------------
| 3 | 16|132|243|endofprevious"}{"name":...
+---+---+---+---+-----------------------

标题很简单:

  • 3 表示边界数
  • 16、132 和 243 表示每个边界的位置,它们对应于新对象(或列表)的左括号

然后是缓冲区本身。

收到这样的数据包后,可以解析以下条目:

  • previous + current[0:16]
  • current[16:132]
  • current[132:243]

current[243:]为下一个数据包保存(尽管您总是可以尝试解析它以防它完成)。

这样,数据包是自动同步的,并且没有模糊检测,它包含所有的故障情况。

请注意,数据包中可能存在0边界。它只是意味着一个对象足够大,可以跨越多个数据包,您只需要暂时累积。

我建议将数字表示“固定”(例如,每个 4 个字节)并确定字节顺序(您的机器的字节顺序)以轻松地将它们转换为二进制文件。我相信开销相当小(每个条目 4 个字节 + 4 个字节,因为{"name":""}已经是 11 个字节)。

于 2012-02-14T07:51:13.073 回答
3

逐个字符遍历缓冲区并手动检查:

  • 字母字符的存在
    • 在字符串之外(但要小心,"可以用 转义\
    • 不属于null,truefalse
    • 不是 aeE内部看起来像带指数的数字文字
  • 一个数字在字符串之外但紧接在一个数字之后"

...并非包罗万象,但我认为它涵盖了足够多的情况,可以在消息截断点或相当接近消息截断点相当可靠地中断解析。

它正确地接受:

{"name": "samL
{"name": "sam0
{"name": "sam", 0
{"name": true

作为有效的 JSON 片段,但捕获:

{"name": "sam"L
{"name": "sam"0
{"name": "sam"true

作为不可接受的。

因此,以下输入都将导致完整的尾随对象被成功解析:

1. {"name": "samp{"name": "aardvark"}
   //            ^ ^
   //            A B    - B is point of failure.
   //                     Stripping leading `{` and scanning for the first
   //                      free `{` gets us to A. (*)
   {"name": "aardvark"}

2. {"name": "samp{"0": "abc"}
   //            ^ ^
   //            A B    - B is point of failure.
   //                     Stripping and scanning gets us to A.
   {"0": "abc"}

3. {"name":{ "samp{"0": "abc"}
   //      ^      ^ ^
   //      A      B C   - C is point of failure.
   //                     Stripping and scanning gets us to A.
   { "samp{"0": "abc"}
   //     ^ ^
   //     B C           - C is still point of failure.
   //                     Stripping and scanning gets us to B.
   {"0": "abc"}

我的实现通过了一些相当彻底的单元测试。不过,我想知道是否可以在不增加复杂性的情况下改进方法本身。


* 实际上,我没有寻找前导"{",而是在每条消息前附加了一个标记字符串,这使得“剥离和扫描”部分更加可靠。

于 2012-02-14T02:53:59.693 回答
0

只需查看 expat 或其他流式 xml 解析器。如果不是,jsoncpp的逻辑应该是类似的。(如果需要,请该库的开发人员改进流式阅读。)

换句话说,从我的角度来看:

  1. 如果您的某些网络(不是 JSON)数据包丢失,则不是 JSON 解析器的问题,只需使用更可靠的协议或发明您自己的协议。然后才通过它传输 JSON。

  2. 如果 JSON 解析器报告错误并且此错误发生在最后一个解析的令牌上(流中没有更多数据但预期) - 累积数据并重试(此任务应由库本身完成)。

    有时它可能不会报告错误。例如,当您传输 123456 并且仅收到 123 时。但这不符合您的情况,因为您不会在单个 JSON 数据包中传输原始数据。

  3. 如果流包含有效数据包和半接收数据包,则应为每个有效数据包调用一些回调。

  4. 如果 JSON 解析器报告错误并且它确实是无效的 JSON,则应关闭流并在必要时再次打开。

于 2012-12-27T09:44:05.907 回答