我的新浪微博:http://weibo.com/freshairbrucewoo。
歡迎大家相互交流,共同提高技術。
JSON (JavaScript Object Notation)是一種數據交換格式,是以JavaScript為基礎的數據表示語言,是在以下兩種數據結構的基礎上來定義基本的數據描述格式的:1) 含有名稱/值對的集合;2) 一個有序的列表。對於 JSON,其部分數據結構的BNF 定義如下所示。形如{“name”:”ldxian”,”age”:23}就表示一個JSON 對象,其有兩個屬性,值分別為ldxian和23。其余的如數字、注釋等跟其他編程語言差不多。下面就開始看看facebook的thrift是怎樣實現json協議的。
首先看看Thrift的數據類型和JSon數據類型的對應關系:
(1) Thrift的所有整數類型JSon都作為數字表示;
(2) Thrift的double類型也作為JSon的數字表示,特殊的值用字符串表示,如下:
a. NaN表示不是數字值;
b. Infinity表示正無窮大;
c. –Infinity表示負無窮大。
(3) Thrift的字符串表示為JSon的字符串並加上一些轉義字符;
(4) Thrift的二進制值並編碼為base64編碼然后作為JSon的字符串;
(5) Thrift的結構體表示為JSon的對象,字段ID作為key,字段值用一個單一的鍵值對的JSon對象表示。鍵是一個簡短的字符串代表特定的類型,接着就是值。有效的類型標識是:"tf"代表 bool,"i8" 表示 byte,"i16"表示16位整數,"i32"表示32位整數,"i64"表示64位整數,"dbl"表示雙精度浮點型,"str" 表示字符串(包括二進制),"rec"表示結構體 ("records"),"map"表示 map,"lst" 表示 list, "set" 表示set。
(6) Thrift的lists和sets被表示為JSon的array(數組),其中數組的第一個元素表示Thrift元素的數據類型,數組第二值表示后面Thrift元素的個數。接着后面就是所有的元素。
(7) Thrift的maps被表示為JSon的array,其中前兩個值分別表示鍵和值的類型,跟着就是鍵值對的個數,接着就是包含具體鍵值對的JSon對象了。注意了Json的鍵只能是字符串,這就是要求Thrift的maps類型的鍵必須是數字和字符串的,並且數字需要序列化為字符串。
(8) Thrift的messages(消息)被表示為JSon的array,前四個元素分別代表協議版本、消息名稱、消息類型和序列ID。
知道例如了每一個數據類型的對應關系了,並且知道特殊情況下怎樣轉換對應的數據,那么在實現對Thrift的傳輸數據進行Json編碼就是很容易的事情了,就是按照對應的關系先進行Json編碼然后才寫入傳輸層由他發送到網絡中的另一端,而另一端接收到數據的時候就按照Json協議解碼就可以了,整個過程大概就是這樣了。下面分析實現Json編碼協議過程中一些比較重要的代碼。
Json編碼協議的實現和前面介紹的協議都一樣需要實現同樣的接口給上層調用。在真正實現這些函數以前,它做了很多的准備工作,主要定義一些靜態數據和幫助函數,靜態數據主要是一些特殊字符或符號的代表常量和一些轉義字符的表示等,幫助函數主要就是數據類型名稱和id的相互轉換、根據類型的短的表示字符串得到具體的數據類型等。這些類容都很直觀且簡單,一看代碼就懂了,就不具體分析介紹了。下面主要介紹幾個重點函數的實現過程。還是按照前面協議的慣例,第一個分析開始寫消息的函數writeMessageBegin,代碼如下:
1 uint32_t result = writeJSONArrayStart();//寫json數組開始 2 3 result += writeJSONInteger(kThriftVersion1);//把協議版本號轉換為json格式后寫入 4 5 result += writeJSONString(name);//把消息名稱轉化為json字符串后寫入 6 7 result += writeJSONInteger(messageType);//同理:消息類型 8 9 result += writeJSONInteger(seqid);//序列號
這里貼出來的代碼就不在是完成的函數定義了,只是貼出一些主要代碼,以后也會按照這種方式了,前面的主要為了完整性的考慮,后面的分析可能更多需要結合源代碼一起了。現在的目的就是提供思路和分析程序設計的思路、思想,想要了解完整的實現還必須的去看完整的源代碼。
通過前面介紹的Thrift的數據類型和Json數據類型的對於關系可知,發送一個messages的數據結構對於json來說是轉換為一個數組,所以writeMessageBegin函數的實現首先調用函數writeJSONArrayStart寫入一個數組開始的表示符號‘[’,同樣在介紹的時候會調用相應的函數寫入數組結束符號,后面就不單獨介紹了。然后按照messages轉化為json的格式一次寫入了協議版本、消息名稱、消息類型和序列號。其中消息名稱是字符串數據類型,所以轉化為json的字符串發送;而協議版本、消息類型和序列號都是整數所以轉化為json的數字傳輸。其實這個實現過程和前面介紹的協議都差不多,不同的就是內容采用了json格式來發送,所以下面主要看看怎樣把這些數據類型轉化為json,先看看writeJSONInteger函數,它是把整數轉化為數字,主要代碼如下:
1 uint32_t result = context_->write(*trans_);//默認什么都不做 2 3 std::string val(boost::lexical_cast<std::string>(num));//調用boost庫的類型轉換函數把數字轉換為字符串 4 5 bool escapeNum = context_->escapeNum();//是否是轉義字符,默認false 6 7 if (escapeNum) {//不會執行,如果是轉義字符就用json的字符串分隔符分隔開 8 9 trans_->write(&kJSONStringDelimiter, 1); 10 11 result += 1; 12 13 } 14 15 trans_->write((const uint8_t *)val.c_str(), val.length());//寫入轉換后的字符串到傳輸層 16 17 result += val.length();//寫入長度計算 18 19 if (escapeNum) { 20 21 trans_->write(&kJSONStringDelimiter, 1); 22 23 result += 1; 24 25 }
其實就是把整數轉換成字符串了,然后寫入傳輸層。繼續看看字符串的轉換寫入函數writeJSONString,如下:
1 result += 2; // 這長度是兩個雙引號的,字符串的分隔符 2 3 trans_->write(&kJSONStringDelimiter, 1);//寫入字符串開始的雙引號 4 5 std::string::const_iterator iter(str.begin());//字符串遍歷用的迭代器 6 7 std::string::const_iterator end(str.end());//結束處 8 9 while (iter != end) { 10 11 result += writeJSONChar(*iter++);//一個字符一個字符的寫入json字符 12 13 } 14 15 trans_->write(&kJSONStringDelimiter, 1);//字符串介紹的雙引號
由上面代碼可以看出寫入字符串是一個一個字符的寫入的,為什么需要這樣呢?因為字符串可能包含特殊的字符,例如轉義字符,所以對於每一個寫入的字符都需要判斷,如果是特殊字符就需要特殊處理(轉義字符處理)。繼續看看字符寫入writeJSONChar,這個函數具體的體現了特殊字符的處理,如下:
1 if (ch >= 0x30) {//字符的整數值如果大於0x30 2 3 if (ch == kJSONBackslash) { // ascii編碼大於等於0x30的特殊字符只有'\' 4 5 trans_->write(&kJSONBackslash, 1);//轉義字符 6 7 trans_->write(&kJSONBackslash, 1);//寫入'\' 8 9 } 10 11 else { 12 13 trans_->write(&ch, 1);//其余直接寫入 14 15 } 16 17 } 18 19 else { 20 21 uint8_t outCh = kJSONCharTable[ch];//ascii編碼在前0x30的用一個表格來對應怎樣處理 22 23 if (outCh == 1) {//表格中的值是1就直接寫入 24 25 trans_->write(&ch, 1); 26 27 } 28 29 else if (outCh > 1) {//表格中的值大於1就是需要轉義字符 30 31 trans_->write(&kJSONBackslash, 1);//寫入轉義字符/ 32 33 trans_->write(&outCh, 1);//寫入具體的字符 34 35 } 36 37 else {//為0的就先寫入轉義序列,在轉換為16進制寫入 38 39 return writeJSONEscapeChar(ch);//先寫入\00在寫入兩位16進制xx 40 41 } 42 43 }
這段代碼主要判斷字符的ascii編碼是否大於48(0x30),如果大於等於的話除了轉義字符(\)需要轉義字符以外(\\)都直接寫入,小於的話就查詢一個表中的值來決定這樣處理,處理方式請看代碼注釋。
寫入一個消息數據大致過程和涉及到的json編碼寫入都已經介紹完了,下面就開始看看對於的讀取一個消息數據的過程和涉及到json解碼的一些內容,先看讀消息函數readMessageBegin,代碼如下:
1 uint32_t result = readJSONArrayStart();//讀取數字的開始符號[ 2 3 uint64_t tmpVal = 0; 4 5 result += readJSONInteger(tmpVal);//讀取協議版本 6 7 if (tmpVal != kThriftVersion1) {//版本不對就拋出異常 8 9 throw TProtocolException(TProtocolException::BAD_VERSION, "Message contained bad version."); 10 11 } 12 13 result += readJSONString(name);//讀取消息名稱 14 15 result += readJSONInteger(tmpVal);//讀取消息類型 16 17 messageType = (TMessageType)tmpVal; 18 19 result += readJSONInteger(tmpVal);//讀取序列號 20 21 seqid = tmpVal;
可以看出消息寫入函數先寫入什么讀取函數就對於先讀取什么。這里涉及的讀取json格式的數據也包括整數和字符串,那么先看看怎么解碼的json數字,函數readJSONInteger主要代碼如下:
1 std::string str; 2 3 result += readJSONNumericChars(str);//讀取json數字字符串 4 5 try { 6 7 num = boost::lexical_cast<NumberType>(str);//字符串轉換為數字類型數據 8 9 } 10 11 catch (boost::bad_lexical_cast e) {//可能拋出異常,處理異常 12 13 throw new TProtocolException(TProtocolException::INVALID_DATA, 14 15 "Expected numeric value; got \"" + str + "\""); 16 17 }
主要代碼比較少,就是調用另一個函數先把代表數字的字符串一個一個的讀取出來(讀取的時候會判斷是不是json有效的數字字符),然后通過boost的庫函數轉換為具體的一種數字類型(如int、double),轉換可能拋出異常(無效的數據)。繼續看看字符串的解碼函數readJSONString,主要代碼如下:
1 readJSONSyntaxChar(kJSONStringDelimiter);//讀取一個字符並且判斷是都是傳遞進去的" 2 3 while (true) { 4 5 ch = reader_.read();//讀取一個字符 6 7 if (ch == kJSONStringDelimiter) {//如果是字符串分隔符("),就結束了一一個字符串的讀取 8 9 break; 10 11 } 12 13 if (ch == kJSONBackslash) {//判斷是不是反斜杠 14 15 ch = reader_.read();//是就在讀取下一個字符 16 17 if (ch == kJSONEscapeChar) {//是不是轉義字符序列開始符號(u) 18 19 result += readJSONEscapeChar(&ch);//是就讀取轉義序列開始符號(00xx) 20 21 } 22 23 else { 24 25 size_t pos = kEscapeChars.find(ch);//查找是不是轉義字符之一(\"\\bfnrt) 26 27 if (pos == std::string::npos) {//不是就拋出無效數據異常 28 29 throw TProtocolException(TProtocolException::INVALID_DATA, "Expected control char, got '" + 30 31 std::string((const char *)&ch, 1) + "'."); 32 33 } 34 35 ch = kEscapeCharVals[pos];//根據拿到這個轉義字符 36 37 } 38 39 } 40 41 str += ch;//處理后的字符加入到解碼后的字符串中,也就是最終解碼結果的字符串 42 43 }
字符串的解碼也是一個字符一個字符的,和寫入一樣,每一個字符都有可能是特殊字符(轉義字符或是需要被轉義的字符),如果是特殊字符就需要處理后才能加入解碼后的結果字符串中。
總結:上面對於json的分析只針對了消息數據結構的編碼寫入和對於的讀取解碼,是一個完整的json協議通信了,當然還有其他的數據結構(如struct、map、set、list和double等),它們也有自己需要處理的特殊地方,但是總體的流程都是一樣的,而且難度不大,只要按照既定好的協議json編碼寫入和json解碼讀取就行了。上面的分析提供個完整分析的思路,想完完全全了解整個thrift采用的json協議還是讀取源代碼(TJSONProtocol.h和TJSONProtocol.cpp文件)。其中源代碼里面還有一點知識就是thrift采用自己實現的base64編碼和解碼,需
要自己實現的可以借鑒其實現。Json協議類分析到此結束!
下一個協議分析更NB:稠密協議類TDenseProtocol。請聽下回分解!~