1. 问题陈述
首先, json报文语法格式是正确无误的, 使用json在线工具解析ok. 而且出现的问题现象是: 相同的报文格式, 有的解析成功; 而有的解析失败。比如报文:
[
{
"name": "lixiaogang5",
"sex": "男",
"address": "guizhousheng"
}
]
备注: 这个JSON仅用于本例说明, 并非实际项目中的JSON格式字符串数据.
概率性的某些同格式的json解析失败。解析过程:
document.Parse(pMsg);
if(document.HasParseError())
{
// 错误处理分支
}
因为没有增加RapidJSON解析失败的可视化字符串打印提示,所以无法查看具体报错原因。需要增加RapidJSON解析失败的错误码打印提示。
2. 问题排查
粗略扫描下RapidJSON源码, 共提供了以下几个与解析失败的提示信息有关的API, 现增加到项目工程中, 重新Makefile编译并替换服务器, 重启服务并复现该问题, 观察错误日志打印.
LOG_PRT("GetParseError:[%d], GetErrorOffset:[%ld], GetParseError_En[%s], HasParseError[%d]",
document.GetParseError(), document.GetErrorOffset(),
(const RAPIDJSON_ERROR_CHARTYPE*)GetParseError_En((ParseErrorCode)document.GetParseError()),
document.HasParseError());
得到RapidJSON可视化字符串提示信息如下:
在RapidJSON源码中, 共有以下几种可视化字符串提示信息,
//! Maps error code of parsing into error message.
/*!
\ingroup RAPIDJSON_ERRORS
\param parseErrorCode Error code obtained in parsing.
\return the error message.
\note User can make a copy of this function for localization.
Using switch-case is safer for future modification of error codes.
*/
inline const RAPIDJSON_ERROR_CHARTYPE* GetParseError_En(ParseErrorCode parseErrorCode) {
switch (parseErrorCode) {
case kParseErrorNone: return RAPIDJSON_ERROR_STRING("No error.");
case kParseErrorDocumentEmpty: return RAPIDJSON_ERROR_STRING("The document is empty.");
case kParseErrorDocumentRootNotSingular: return RAPIDJSON_ERROR_STRING("The document root must not be followed by other values.");
case kParseErrorValueInvalid: return RAPIDJSON_ERROR_STRING("Invalid value.");
case kParseErrorObjectMissName: return RAPIDJSON_ERROR_STRING("Missing a name for object member.");
case kParseErrorObjectMissColon: return RAPIDJSON_ERROR_STRING("Missing a colon after a name of object member.");
case kParseErrorObjectMissCommaOrCurlyBracket: return RAPIDJSON_ERROR_STRING("Missing a comma or '}' after an object member.");
case kParseErrorArrayMissCommaOrSquareBracket: return RAPIDJSON_ERROR_STRING("Missing a comma or ']' after an array element.");
case kParseErrorStringUnicodeEscapeInvalidHex: return RAPIDJSON_ERROR_STRING("Incorrect hex digit after \\u escape in string.");
case kParseErrorStringUnicodeSurrogateInvalid: return RAPIDJSON_ERROR_STRING("The surrogate pair in string is invalid.");
case kParseErrorStringEscapeInvalid: return RAPIDJSON_ERROR_STRING("Invalid escape character in string.");
case kParseErrorStringMissQuotationMark: return RAPIDJSON_ERROR_STRING("Missing a closing quotation mark in string.");
case kParseErrorStringInvalidEncoding: return RAPIDJSON_ERROR_STRING("Invalid encoding in string.");
case kParseErrorNumberTooBig: return RAPIDJSON_ERROR_STRING("Number too big to be stored in double.");
case kParseErrorNumberMissFraction: return RAPIDJSON_ERROR_STRING("Miss fraction part in number.");
case kParseErrorNumberMissExponent: return RAPIDJSON_ERROR_STRING("Miss exponent in number.");
case kParseErrorTermination: return RAPIDJSON_ERROR_STRING("Terminate parsing due to Handler error.");
case kParseErrorUnspecificSyntaxError: return RAPIDJSON_ERROR_STRING("Unspecific syntax error.");
default: return RAPIDJSON_ERROR_STRING("Unknown error.");
}
}
现在报错提示可视化字符串是: The document root must not be followed by other values. 翻译过来即:“文档根目录后不能跟随其他值”, 说明解析失败的JSON格式化字符串后面应该是有不可见字符数据,才导致的RapidJSON解析失败. 最为重要的是GetErrorOffset()函数提示错误的位置是559, 即:GetErrorOffset: 559 . 当前报文大小刚好是559个字节(将json串复制到Nodepad++ 中即可在底部提示当前字符串长度. ). 因此, 更加可以确定问题一定是在格式化后的JSON串末尾, 具体是啥字符, 暂时未知, 因为是一个不可终端显示的特殊字符。
我们(模块2)待解析的JSON数据kafka broker上面消费到的, 因此, 为了确保获取到最原始的数据格式信息, 使用kafka发行包中自带的脚本工具 kafka-run-class.sh 去读取kafka log文件中的数据, 此处的时间戳(1589197149547)是解析失败的对应JSON数据, 将log文件中的数据筛选出来并追加写入1.txt文件中, 然后直接vim进去查看, 或是使用cat -v参数 1.txt 都可查看到文件中的编码格式以及具体数据。
/home/soft/NodeServer/kafka/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /home/ssd/kafka/SINGLE_TRACE_INFO-2/00000000000000000000.log --print-data-log |grep "1589197149547" >>1.txt
使用cat -v查看的效果如下所示, 可以看到在JSON格式字符串的末尾有一个特殊字符: ^A.
使用vim打开查看到的效果如下:
到这里时候, 问题已经定位到了, 接下来便是问题的解决方案。
备注: (1). 模块1中生产到kafka中的数据是来自其上一层的上一层(相机)模块. (2). 模块(1)使用的是cjson库, 是能够解析所有格式的报文的(包括以^A, ^M等特殊字符结尾的JSON报文). (3) 若JSON报文的内部有其他混合编码问题, 则该部分会解析失败.
3. 解决方案
首先, 肯定是让数据的源头(相机取流模块)定位结尾特殊字符产生的原因, 从根本上解决掉. 其次, 增加自己模块的代码健壮性, 对RapidJSON的解析函数 doc.Parse()进行增强, 即添加一些解析的标志位组合.(如Linux下的文件句柄增加 非阻塞O_NONBLOCK 属性标志同理), doc.Parse() 函数的解析参数标志位有以下几种:
enum ParseFlag {
kParseNoFlags = 0, //!< No flags are set.
kParseInsituFlag = 1, //!< In-situ(destructive) parsing.
kParseValidateEncodingFlag = 2, //!< Validate encoding of JSON strings.
kParseIterativeFlag = 4, //!< Iterative(constant complexity in terms of function call stack size) parsing.
kParseStopWhenDoneFlag = 8, //!< After parsing a complete JSON root from stream, stop further processing the rest of stream. When this flag is used, parser will not generate kParseErrorDocumentRootNotSingular error.
kParseFullPrecisionFlag = 16, //!< Parse number in full precision (but slower).
kParseCommentsFlag = 32, //!< Allow one-line (//) and multi-line (/**/) comments.
kParseNumbersAsStringsFlag = 64, //!< Parse all numbers (ints/doubles) as strings.
kParseTrailingCommasFlag = 128, //!< Allow trailing commas at the end of objects and arrays.
kParseNanAndInfFlag = 256, //!< Allow parsing NaN, Inf, Infinity, -Inf and -Infinity as doubles.
kParseDefaultFlags = RAPIDJSON_PARSE_DEFAULT_FLAGS //!< Default parse flags. Can be customized by defining RAPIDJSON_PARSE_DEFAULT_FLAGS
};
本次需要增加 kParseStopWhenDoneFlag(在从流解析完一个完整的JSON根之后, 停止进一步处理流的其余部分. 解析器将不会生成kParseErrorDocumentRootNotSingular错误) 标志, 因此, 可以规避RapidJSON解析Root之后的特殊字符而造成的失败.
document.Parse<rapidjson::kParseStopWhenDoneFlag>(SerializeJsonMessage);
if(document.HasParseError())
{
//错误分支逻辑处理. . .
}
4. 结论
使用修改后的代码经重编译后, 替换服务器并运行, 在该JSON问题报文源头还未解决情况下, 仍然能够解析成功不报错. 当然, JSON报文源头的模块一定得解决此问题, 因为不确定下一次的报文又是以什么特殊字符结尾.