`
thecloud
  • 浏览: 881798 次
文章分类
社区版块
存档分类
最新评论

Thrift之TProtocol类体系原理及源码详细解析之紧凑协议类TCompactProtocolT(TCompactProtocol)

 
阅读更多

我的新浪微博:http://weibo.com/freshairbrucewoo

欢迎大家相互交流,共同提高技术。


这个协议类采用了zigzag编码,这种编码是基于Variable-lengthquantity编码提出来的,因为Variable-lengthquantity编码对于负数的编码都需要很长的字节数,而zigzag编码对于绝对值小的数字,无论正负都可以采用较少的字节来表示,充分利用了Varint技术。所以这个协议类采用zigzag编码可以节省传输空间,使数据的传输效率更高。至于zigzag具体的编码实现方式可以网上查查,其实就是把从低位到最后一个还存在1(二进制)的最高位表示出来就可以了。这个协议类对外提供的方法和上面介绍的二进制协议相同,这样可以很方便使用者从一种协议改变到另一种协议。

下面我同样结合scribe提供的Log方法来分析这个协议类的功能,不过不会像上面二进制协议在把整个过程分析了,我只会分析与协议相关的部分了,分析一些比较难懂的一些函数功能,分析的思路还是按照函数调用过程来分析。

首先还是分析writeMessageBegin函数,下面是这个函数的实现代码:

template<classTransport_>uint32_tTCompactProtocolT<Transport_>::writeMessageBegin(

conststd::string&name,constTMessageTypemessageType,constint32_tseqid){

uint32_twsize=0;

wsize+=writeByte(PROTOCOL_ID);//写入这个协议的产品ID号:为0x82

wsize+=writeByte((VERSION_N&VERSION_MASK)|(((int32_t)messageType<<TYPE_SHIFT_AMOUNT)&TYPE_MASK));//写入此协议的版本号和消息类型:前3位是消息类型,后面5位是协议版本号

wsize+=writeVarint32(seqid);//写入请求序列号

wsize+=writeString(name);//写入消息名称(也就是函数调用名称)

returnwsize;//返回写入的大小,多少字节

}

因为这些协议类都是模板类,所以每一个函数也就是模板函数了。函数具体的功能代码里有详细注释,其中的writeByte函数就是写入一个字节到服务器。这里与二进制协议不同的是这里写入请求序列号(也就是对于所有的整型数)都调用的writeVarint32函数,这个函数就是采用zigzag编码写入整型数到服务器,代码如下:

template<classTransport_>uint32_tTCompactProtocolT<Transport_>::writeVarint32(uint32_tn){

uint8_tbuf[5];//对于一个整数,zigzag编码最大采用5个字节保存

uint32_twsize=0;

while(true){

if((n&~0x7F)==0){//判断除了最低7位是否还有其他高位为1(二进制)

buf[wsize++]=(int8_t)n;//没有了代表着就是最后一个字节

break;//退出循环

}else{

buf[wsize++]=(int8_t)((n&0x7F)|0x80);//取最低7位加上第8位(为1代表后续还有字节属于这个整数,为0代表这是这个整数的最后一个字节了。

n>>=7;//移走已经编码的位数

}

}

trans_->write(buf,wsize);//写入编码的字节数

returnwsize;//返回写入的字节数

}

这个函数的功能就是对整数进行Variable-lengthquantity编码后写入,如果为负数需要处理。如果不处理那么每一个负数都需要5个字节来编码,因为最高位表示符号位,而负数的符号位用1表示(也就是说负数的最高位永远为1)。处理的方式也很简单(就是zigzag编码),就是把最高位(符号位)移动到最低位,最低位到次高位一次向高位移动一位,代码如下(就一句就实现了):

template<classTransport_>

uint32_tTCompactProtocolT<Transport_>::i32ToZigzag(constint32_tn){

return(n<<1)^(n>>31);

}

上面写入整数和处理负数都是针对的32位的,当然也有64位的相应函数,实现方式相同。我们在回到writeMessageBegin函数,里面还有一个writeString函数用来写入一个字符串的,与二进制不同的是写入字符串长度也是采用了可变长度编码的方式写入,然后写入字符串的具体数据,它是调用另一个函数writeBinary写入,writeBinary实现代码如下:

template<classTransport_>

uint32_tTCompactProtocolT<Transport_>::writeBinary(conststd::string&str){

uint32_tssize=str.size();

uint32_twsize=writeVarint32(ssize)+ssize;//写入字符串的长度并计算写入的长度(包括字符串的长度)

trans_->write((uint8_t*)str.data(),ssize);//写入字符串的数据

returnwsize;

}

写消息函数分析完毕以后我们在来看看对应的读消息函数readMessageBegin,看这个函数必须和写入消息的函数对应起来看,不然就不能理解它读取和处理的流程代码,具体实现如下代码:

template<classTransport_>uint32_tTCompactProtocolT<Transport_>::readMessageBegin(

std::string&name,TMessageType&messageType,int32_t&seqid){

uint32_trsize=0;

int8_tprotocolId;

int8_tversionAndType;

int8_tversion;

rsize+=readByte(protocolId);//读取协议产品ID

if(protocolId!=PROTOCOL_ID){//判断是不是这个协议的产品ID号,不是就抛出异常

throwTProtocolException(TProtocolException::BAD_VERSION,"Badprotocolidentifier");

}

rsize+=readByte(versionAndType);//读取此协议的版本号和消息类型

version=(int8_t)(versionAndType&VERSION_MASK);//取出协议版本号

if(version!=VERSION_N){//判断是不是对应的协议版本号,不是抛出异常

throwTProtocolException(TProtocolException::BAD_VERSION,"Badprotocolversion");

}

messageType=(TMessageType)((versionAndType>>TYPE_SHIFT_AMOUNT)&0x03);//取出消息类型

rsize+=readVarint32(seqid);//读取请求序列号

rsize+=readString(name);//读取消息名称(函数名称)

returnrsize;//返回读取的长度(字节)

}

通过对照写入消息的函数就很容易理解,因为你写入什么我就读什么并且判断是不是相同协议写入的,具体分析可以看上面的代码和详细的注释。而且还有一点就是具体的写入数据类型的函数也是采用对应类型的读函数,例如读可变长整型写入就是采用可变长读函数readVarint32,写字符串对应读字符串函数readString,对照相应的写入函数来看这些读数据函数就非常好理解了,就不具体分析这些读函数了。

下面在分析几个复合数据类型的写入函数,因为这些写入函数存在一定技巧不容易(或者说不那么直观吧)理解清楚。首先看看struct类型的数据写入的过程,它分为写入开始、中间处理和写入结束。下面是开始写入struct的代码:

template<classTransport_>

uint32_tTCompactProtocolT<Transport_>::writeStructBegin(constchar*name){

(void)name;

lastField_.push(lastFieldId_);//把最后写入的字段ID压入堆栈

lastFieldId_=0;//重新设置为0

return0;

}

这开始写入的函数没有做什么具体的工作,只是把最后写入的字段ID压入堆栈,这样做的目的是处理那种struct嵌套的数据结构类型。

Struct里面的是一个一个的字段,所以根据struct的字段个数分别调用字段写入函数依次写入,字段写入函数定义如下:

template<classTransport_>int32_tTCompactProtocolT<Transport_>::writeFieldBeginInternal(

constchar*name,constTTypefieldType,constint16_tfieldId,int8_ttypeOverride){

(void)name;//为了防止编译器产生警告信息

uint32_twsize=0;

//如果存在对于对应的类型就转换为对应的

int8_ttypeToWrite=(typeOverride==-1?getCompactType(fieldType):typeOverride);

//检查字段ID是否使用了增量编码

if(fieldId>lastFieldId_&&fieldId-lastFieldId_<=15){//如果使用了增量编码并增量且小于等于15

wsize+=writeByte((fieldId-lastFieldId_)<<4|typeToWrite);//字段ID和数据类型一起写入

}else{//否则单独写入

wsize+=writeByte(typeToWrite);//写入数据类型

wsize+=writeI16(fieldId);//写入字段ID

}

lastFieldId_=fieldId;//保存写入字段ID为最后一个写入的ID

returnwsize;//返回写入的长度

}

当结构体里面的每一个字段都写入以后还需要调用writeStructEnd函数来处理结束一个struct的写入,主要处理是字段ID的相关内容,实现代码如下:

template<classTransport_>uint32_tTCompactProtocolT<Transport_>::writeStructEnd(){

lastFieldId_=lastField_.top();//取得最后一次压入堆栈的字段ID

lastField_.pop();//弹出以取得的字段ID

return0;

}

同样的结构体也有对应的读取函数,具体实现就不在具体分析了!下面继续分析一些特殊的处理代码,首先看看负数在进行zigzag编码前怎样处理,对于32位和64位都是一句代码就搞定,如下代码:

return(n>>1)^-(n&1);

这句代码的作用就是把最高位的符号位移动到最低位,然后最低位到次高位依次向高位移动一位,这样就避免了所有负数都需要最长的字节来编码。在看看读可变长编码写入整型数的函数,32位和64位都是相同的实现,因为32位也是调用64位的函数实现的,实现代码如下:

template<classTransport_>uint32_tTCompactProtocolT<Transport_>::readVarint64(int64_t&i64){

uint32_trsize=0;

uint64_tval=0;

intshift=0;

uint8_tbuf[10];//64位采用zigzag编码最长可能是10字节

uint32_tbuf_size=sizeof(buf);

constuint8_t*borrowed=trans_->borrow(buf,&buf_size);//并不是所有transport都支持

if(borrowed!=NULL){//快路径,要读的数据已经在缓存中

while(true){

uint8_tbyte=borrowed[rsize];

rsize++;

val|=(uint64_t)(byte&0x7f)<<shift;//取得对应编码数据的7

shift+=7;//7

if(!(byte&0x80)){//是否还有属于这个数的编码字节,字节的最高位表示:0表示没有了

i64=val;//读取解码后的真正有效值

trans_->consume(rsize);//消耗了多少字节,即表示这个编码用了多少字节

returnrsize;

}

//检查编码数据是否超过了最长限制,是就抛出一个无效的异常

if(UNLIKELY(rsize==sizeof(buf))){

throwTProtocolException(TProtocolException::INVALID_DATA,"Variable-lengthintover10bytes.");

}

}

}

else{//慢路径,要读的数据还没有存在缓存中

while(true){

uint8_tbyte;

rsize+=trans_->readAll(&byte,1);//读取一个字节

val|=(uint64_t)(byte&0x7f)<<shift;//取得7位的编码数据

shift+=7;

if(!(byte&0x80)){

i64=val;

returnrsize;

}

if(UNLIKELY(rsize>=sizeof(buf))){//同样检查数据的有效性:最大字节长度不超过10个字节

throwTProtocolException(TProtocolException::INVALID_DATA,"Variable-lengthintover10bytes.");

}

}

}

}

由于采用了可变长度编码的原因,所以不知道一次性应该读取多少个字节是一个完整的数据。为了读取效率所以一次性直接读取最长可能的字节数量,也就是10字节,因为64位最长的可变长编码就是10字节长,然后根据实际消耗的字节数从读取元跳过已经消耗的字节数。不过底层的传输层,有些协议可能不支持这种预读取方式,所以就只有一个字节一个字节的读取。

这个协议最大的特点就是采用了可变长度编码,并且采用zigzag编码处理负数总是需要采用最长的编码字节的问题,所以相对于比较二进制而言效率提高了不少。

分享到:
评论

相关推荐

    thrift源码

    thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码thrift源码...

    使用wireshark抓取thrift协议接口调用

    使用wireshark抓取thrift协议接口调用

    thrift包及其源码

    thrift0.9.1版本的jar包,还有源码,可供大家参考学习使用。

    Apache Thrift Java实战源码,包含了客户端和服务端源码

    Apache Thrift Java实战源码,包含了客户端和服务端源码,客户端和服务端是分开的,如果需要放到一个工程,直接把Client.java文件复制到服务端运行即可。

    thrift源码+DEMO+简单教程

    thrift源码+DEMO+简单教程

    windows下vs2010编译的thrift,包含lib和cpp源码

    windows下vs2010完美编译thrift,包含lib文件和src源码,thrift版本为thrift-0.11.0

    Thrift之C++初体验

    thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, ...

    libthrift-1.0.0.jar,thrift例子,thrift源码

    libthrift-1.0.0.jar,thrift例子,thrift源码 里面有你想要java版的thrift全部文件

    Apache Thrift 使用说明

    详细介绍了Apache Thrift在Ubuntu以及Windows下基于C++和Java语言的安装和运行。附有小例子,亲自测试通过。所述方法网上应该有教程,但大多零散不统一或者不完整,因此本人整理了一份,特来分享。

    thrift官方代码+与dubbo集成支持原生thrift协议

    thrift官方代码+与dubbo集成支持原生thrift协议

    thrift实现http协议案例

    thrift实现http协议案例,thrift官方好像就一个socket,此案例是通过继承servlet实现http协议通信

    thrift初步了解

    不要认为thrift能在*.thrift文件中定义调度之类的工作 thrift通过IDL(接口描述语言)来生成各种语言需要的接口代码。 执行thrift文件命令: java:thrift-0.9.1.exe --gen java test.thrift java:thrift-0.9.1.exe ...

    支持thrift协议的纯java版的增强工具集合, pojo直接转换为thrift,json/xml转换为thrift

    thrift-enhancer是一组支持thrift协议的加强包,设计...thrift-translator: 提供动态解析idl并生成参数对象的能力,动态生成的参数对象可以自动转换为thrift协议数据,同时提供 thrift与json、xml的双向转换, 动态解析

    thrift-0.9.2.tar.gz

    thrift,Apache Thrift 0.9.2 版本,解压后直接直接安装,可伸缩的跨语言服务开发框架,命令: 解压命令:tar -zxf thrift-0.9.2.tar.gz 安装命令:./configure --with-lua=no && make && make install 查看版本:...

    编译的spark-hive_2.11-2.3.0和 spark-hive-thriftserver_2.11-2.3.0.jar

    spark-hive_2.11-2.3.0...spark-hive-thriftserver_2.11-2.3.0.jar log4j-2.15.0.jar slf4j-api-1.7.7.jar slf4j-log4j12-1.7.25.jar curator-client-2.4.0.jar curator-framework-2.4.0.jar curator-recipes-2.4.0.jar

    thrift 源码(一种可伸缩的跨语言服务的发展软件框架)

    thrift是一种可伸缩的跨语言服务的发展软件框架。它结合了功能强大的软件堆栈的代码生成引擎,以建设服务,工作效率和无缝地与C + +,C#,Java,Python和PHP和Ruby结合。thrift是facebook开发的,我们现在把它作为...

    thrift例子

    NULL 博文链接:https://andilyliao.iteye.com/blog/1931911

    flume通过thrift协议收集日志-Python

    flume通过thrift协议收集日志-Python。博客地址:http://blog.csdn.net/wangshuwei5/article/details/48902757

    基于Scala的Spark Thrift Server设计源码

    本设计源码提供了一个基于Scala的Spark Thrift Server。项目包含12731个文件,主要使用Scala、Java、Python、Shell、JavaScript、CSS、HTML、Ruby和C编程语言。文件类型包括3539个Scala源代码文件、1559个Q文件、...

    thrift-0.9.3.exe

    Thrift最初由facebook开发,07年四月开放源码,08年5月进入apache孵化器。thrift允许你定义一个简单的定义文件中的数据类型和服务接口。以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨...

Global site tag (gtag.js) - Google Analytics