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報文源頭的模塊一定得解決此問題, 因爲不確定下一次的報文又是以什麼特殊字符結尾.