Thrift之TProtocol类体系原理及源码详细解析之JSon协议类TJSONProtocol
我的新浪微博: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。请听下回分解!~