RapidJSON報錯: The document root must not be followed by other values.

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章