thrift

本文介绍了Thrift的基本概念,包括IDL支持的功能、代码生成机制、二进制协议细节及示例程序。涵盖了Thrift的IDL特性、类型映射、代码结构、序列化与反序列化流程等关键内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Thrift浅析

mailto://zhangyan04@baidu.com

IDL层面

功能列表 thrift
是否支持include include
是否支持名字空间 namespace
是否支持typedef
是否支持常量
是否支持枚举
是否支持结构 struct
是否支持定义异常 exception
是否需要字段标识
字段是否支持可选
是否类型嵌套定义
是否支持RPC结构 service
是否支持默认值

类型 C类型
bool bool
byte int8_t
i16 int16_t
i32 int32_t
i64 int64_t
string std::string
double double
map std::map
list std::list
set std::set
binary std::string

  • thrift的include指令还是比较弱的.产生的方式这样的
    • 假设x.thrift中有include "y.thrift"
    • thrift首先编译y.thrift产生.cpp和.h文件[y_types.h]
    • thrift然后编译x.thrift产生.cpp和.h文件[x_types.h]
    • 其中x_types.h中包含y_types.h
    • 和cpp预处理器还是有相当大的差别的,不是完全展开而是单个单元进行翻译
  • thrift支持cpp_include来产生#include指令
  • thrift使用namespace cpp XXX来表示某些语言{'cpp' | 'java' | 'py' | 'perl' | 'rb' | 'cocoa' | 'csharp'}的名字空间,而对于PHP和XSD使用php_namespace和xsd_namespace关键字
  • typedef只能够是针对基本类型和容器类型,而不能够针对自定义类型
  • 常量不允许出现表达式,允许整数,浮点数,字符串,链表,字典,并且允许之间相互嵌套产生层级结构
  • 支持类型中的binary实际上还是string,但是存储上使用len+data来存储
  • 默认所有的字段都是必须的

生成代码层面

使用下面的x.thrift文件

struct SUB { 1:optional i16 a_555, 2:i16 b_555, }

typedef map < i32, i32 > map_t typedef map < list < i32 >, list < i32 > >map_list_t

const i32 CONSTONE = 1 const map_t CONSTMAP = { 1: 2, 3: 4, 5:6 } const map_list_t CONSTMAP2 = {[1, 2, 3]:[4, 5, 6] }

enum XENUM { XXX = 1, YYY = 2, }

exception XEXCEPTION { 1:i32 a_666, 2:i32 b_666, }

struct PACK { 1:i32 a = CONSTONE, 2:i64 b, 3:list < SUB > c, 4:map < SUB, SUB > d, 5:set < SUB > e, 6:string f, 7:binary g, 8:map_t h = CONSTMAP, 9:map_list_t i = CONSTMAP2, 10:bool j, 11:byte k, 12:double l, } 
那么会产生下面4个文件: x_types.cpp,x_types.h,x_constants.cpp和x_constants.h

x_constants.h/.cpp

产生xConstants这个类,里面是所有的thrift文件中提到的常量[但是并没有以const定义]

class xConstants { public: xConstants(); int32_t CONSTONE; map_t CONSTMAP; map_list_t CONSTMAP2; }; //对于这样常量的初始化在构造函数里面完成.... xConstants::xConstants() { CONSTONE = 1; //普通类型常量

CONSTMAP.insert(std::make_pair(1, 2)); //map常量 CONSTMAP.insert(std::make_pair(3, 4)); CONSTMAP.insert(std::make_pair(5, 6));

std::vector tmp0; //map常量 //产生代码完成.. tmp0.push_back(1); tmp0.push_back(2); tmp0.push_back(3);

std::vector tmp1; tmp1.push_back(4); tmp1.push_back(5); tmp1.push_back(6); CONSTMAP2.insert(std::make_pair(tmp0, tmp1)); } 

x_types.h/.cpp

首先产生enum,typedef代码

enum XENUM { XXX = 1, YYY = 2 }; typedef std::map map_t; typedef std::map , std::vector > map_list_t; 
然后产生里面的每一个结构和异常,对于结构和异常产生的代码是相似的,  不过结构产生的代码不继承任何类,而异常产生的代码继承于 ::apache::thrift::TException  .这里面首先看看SUB产生的类.
class SUB { public: //有ascii_fingerprint,和binary_fingerprint..暂时不知道什么用途 //"1D816EBA8445CE709193D471B172E450"; static const char* ascii_fingerprint; //{0x1D,0x81,0x6E,0xBA,0x84,0x45,0xCE,0x70,0x91,0x93,0xD4,0x71,0xB1,0x72,0xE4,0x50}; static const uint8_t binary_fingerprint[16]; //字段使用默认值进行初始化 SUB():a_555(0), b_555(0) {} int16_t a_555; int16_t b_555; //内部存在一个isset类来决定这个字段是否已经设置 struct __isset { __isset() : a_555(false), b_555(false) {} bool a_555; bool b_555; } __isset; //operator<是为了能够使用map容器,用户可能需要自己写这个过程. bool operator == (const SUB & rhs) const bool operator != (const SUB &rhs) const bool operator < (const SUB & ) const; //提供了序列化和反序列化接口 uint32_t read(::apache::thrift::protocol::TProtocol* iprot); uint32_t write(::apache::thrift::protocol::TProtocol* oprot) const; }; 

然后再看看PACK类,这个类里面类型丰富

class PACK { public: PACK() : a(1), b(0), f(""), g(""), j(0), k(0), l(0) { //对于map,list,set都产生代码进行初始化 h.insert(std::make_pair(1, 2)); h.insert(std::make_pair(3, 4)); h.insert(std::make_pair(5, 6));
                
                std::vector tmp2; tmp2.push_back(1); tmp2.push_back(2); tmp2.push_back(3);
                
                std::vector tmp3; tmp3.push_back(4); tmp3.push_back(5); tmp3.push_back(6);
                
                i.insert(std::make_pair(tmp2, tmp3));
                
        }s int32_t a; int64_t b; std::vector<sub> c; //对于list使用std::vector来完成 std::map d; //对于map使用std::map来完成 std::set<sub> e; //对于set 使用std::set来完成 std::string f; //对于string使用std::string来完成 std::string g; //对于binary使用std::string来完成. map_t h; map_list_t i; bool j; int8_t k; double l; }; 
.cpp中是protocol部分,这段代码分析放在二进制协议层面这节分析

问题

  • 没有使用接口和进行字段的设置和获取,在一定程度上简化了访问。但是使得在填写字段和获取字段的时候需要自己手动去访问一下__isset.x来显示修改某个字段已经填写或者是查看是否存在.
  • 序列化
    • 可选字段如果isset设置了的话,那么就会序列化
    • 可选字段如果没有设置isset的话,那么就不会进行序列化
    • 必选无论是否设置isset都回进行序列化
  • 反序列化
    • 看上去好像每一个字段都是可选的
    • 如果字段读取到的话,isset设true
    • 如果字段没有读取到的话,isset设为false
  • 对于默认值会在构造函数时填充
  • 遇到不认识的字段会skip过去

二进制协议层面

图示

  • 结构体中每个field都是自解释的,最后1个字节STOP表示停止位
  • 每一个field开头有1个字节表示类型,比如T_INT16,T_INT32,T_DOUBLE,T_STRUCT.
  • 然后2个字节表示fid即字段标识,之后就是value
  • 对于基本类型的value没有什么可以解释
  • 对于list,1个字节表示类型,4个字节表示list长度,然后再是每个value
  • 对于set,1个字节表示类型,4个字节表示set长度,然后再是每个value
  • 对于set,1个字节表示key类型,1个字节value类型,4个字节表示长度,然后读取方式是1个key1个value.

框架代码

首先看看SUB的read/write

uint32_t SUB::read(::apache::thrift::protocol::TProtocol* iprot) { uint32_t xfer = 0; std::string fname; ::apache::thrift::protocol::TType ftype; int16_t fid; //首先读取struct begin xfer += iprot-&gt;readStructBegin(fname); using ::apache::thrift::protocol::TProtocolException; while (true) { //对于每一个字段,首先读取field begin //读取fname["SUB"],ftype[这个字段是什么类型],fid[字段号] xfer += iprot-&gt;readFieldBegin(fname, ftype, fid); //如果是protocol::T_STOP的话说明到了field end if (ftype == ::apache::thrift::protocol::T_STOP) { break; } switch (fid) { //针对每一个字段会判断类型 case 1: if (ftype == ::apache::thrift::protocol::T_I16) { xfer += iprot-&gt;readI16(this-&gt;a_555); this-&gt;__isset.a_555 = true; } else { //如果字段号和类型不匹配的话,那么会忽略这个长度 xfer += iprot-&gt;skip(ftype); } break; case 2: if (ftype == ::apache::thrift::protocol::T_I16) { xfer += iprot-&gt;readI16(this-&gt;b_555); this-&gt;__isset.b_555 = true; } else { xfer += iprot-&gt;skip(ftype); } break; default: xfer += iprot-&gt;skip(ftype); break; } //然后会读取一个field end xfer += iprot-&gt;readFieldEnd(); } //最后读取一个struct end xfer += iprot-&gt;readStructEnd(); return xfer; }
        
        uint32_t SUB::write(::apache::thrift::protocol::TProtocol* oprot) const { uint32_t xfer = 0; //首先先写一个struct begin,配上"SUB"[class name] xfer += oprot-&gt;writeStructBegin("SUB"); //对于可选字段的话那么会判断可选字段是否设置 if (this-&gt;__isset.a_555) { xfer += oprot-&gt;writeFieldBegin("a_555", ::apache::thrift::protocol::T_I16, 1); xfer += oprot-&gt;writeI16(this-&gt;a_555); xfer += oprot-&gt;writeFieldEnd(); } //开始写字段,field_name,ftype,fid.field_name可能会出现在output中,对于binary不需要,json就需要 xfer += oprot-&gt;writeFieldBegin("b_555", ::apache::thrift::protocol::T_I16, 2); xfer += oprot-&gt;writeI16(this-&gt;b_555); xfer += oprot-&gt;writeFieldEnd(); xfer += oprot-&gt;writeFieldStop(); xfer += oprot-&gt;writeStructEnd(); return xfer; } 
可以看到大致步骤是这样的:  read
  • read_struct_begin
  • read_field_begin(fname, ftype,fid)
  • 如果fid和ftype匹配的话,那么就会读取数据并且置位,否则跳过ftype长度
  • read_field_end
  • read_struct_end
write
  • write_struct_begin(fname)
  • write_field_begin(fname,ftype,fid)
  • 如果可选的话那么会看是否设置,然后write出去
  • write_field_end
  • write_struct_stop并且write_struct_end
SUB是一个简单的结构,然后可以来看看PACK结构。大部分PACK流程和SUB是一样的,但是几个复杂的类型list,map,set,string来看看是怎么读取的,这里举map为例:
if (ftype == ::apache::thrift::protocol::T_MAP) { this-&gt;d.clear(); uint32_t _size9; ::apache::thrift::protocol::TType _ktype10; ::apache::thrift::protocol::TType _vtype11; //读取k,vtype,和size.. //read_map_begin iprot-&gt;readMapBegin(_ktype10, _vtype11, _size9); uint32_t _i13; for (_i13 = 0; _i13 &lt; _size9; ++_i13) { SUB _key14; xfer += _key14.read(iprot); SUB& _val15 = this-&gt;d[_key14]; xfer += _val15.read(iprot); } //read_map_end.. iprot-&gt;readMapEnd(); this-&gt;__isset.d = true; } else { xfer += iprot-&gt;skip(ftype); } 
对于list,set,string都差不多,string和一个基本类型操作相同.

Thrift二进制代码

这里只是分析TBinaryProtocol代码,是专门产生二进制的。还有一些Protocol比如TJSONProtocol是产生Json的。首先看一组基本例程,这里只是拿读写double/string来举例

//可以看到对于8位以上的类型都是network byteorder.. uint32_t TBinaryProtocol::writeDouble(const double dub) { BOOST_STATIC_ASSERT(sizeof(double) == sizeof(uint64_t)); //检查长度为64.. BOOST_STATIC_ASSERT(std::numeric_limits<double>::is_iec559); //检查是否符合IEC 559 standards //bitwise_cast会首先判断长度是否相同,然后回使用一个union结构体来进行转换.. uint64_t bits = bitwise_cast(dub); bits = htonll(bits); trans_-&gt;write((uint8_t*)&bits, 8); return 8; }

//对于string首先写长度,然后写数据.. uint32_t TBinaryProtocol::writeString(const string& str) { uint32_t size = str.size(); uint32_t result = writeI32((int32_t)size); if (size &gt; 0) { trans_-&gt;write((uint8_t*)str.data(), size); } return result + size; }

uint32_t TBinaryProtocol::readDouble(double& dub) { BOOST_STATIC_ASSERT(sizeof(double) == sizeof(uint64_t)); BOOST_STATIC_ASSERT(std::numeric_limits<double>::is_iec559);
        
        uint64_t bits; uint8_t b[8]; trans_-&gt;readAll(b, 8); //还是按照network byteorder来读.. bits = *(uint64_t*)b; bits = ntohll(bits); //转换成为uint64_t dub = bitwise_cast<double>(bits); //最后转换成为double return 8; }
        
        uint32_t TBinaryProtocol::readString(string& str) { uint32_t result; int32_t size; result = readI32(size); return result + readStringBody(str, size); } 
然后下面是一些复合结构的例程,这里使用struct和map为例
namespace protocol {
        
        uint32_t TBinaryProtocol::writeStructBegin(const char* name) { return 0; }
        
        uint32_t TBinaryProtocol::writeStructEnd() { return 0; }
        
        //对于每一个field字段 //1.首先写一个字段的类型,8字节 //2.然后写16字节的field id.... uint32_t TBinaryProtocol::writeFieldBegin(const char* name, const TType fieldType, const int16_t fieldId) { uint32_t wsize = 0; wsize += writeByte((int8_t)fieldType); wsize += writeI16(fieldId); return wsize; }
        
        uint32_t TBinaryProtocol::writeFieldEnd() { return 0; }
        
        //然后对么对于field所有完成之后写一个field stop标记. uint32_t TBinaryProtocol::writeFieldStop() { return writeByte((int8_t)T_STOP); }
        
        //对于map //1.keytype //2.valuetype //3.map的大小. uint32_t TBinaryProtocol::writeMapBegin(const TType keyType, const TType valType, const uint32_t size) { uint32_t wsize = 0; wsize += writeByte((int8_t)keyType); wsize += writeByte((int8_t)valType); wsize += writeI32((int32_t)size); return wsize; }
        
        uint32_t TBinaryProtocol::writeMapEnd() { return 0; }
        
        uint32_t TBinaryProtocol::readStructBegin(string& name) { name = ""; return 0; }
        
        uint32_t TBinaryProtocol::readStructEnd() { return 0; }
        
        uint32_t TBinaryProtocol::readFieldBegin(string& name, TType& fieldType, int16_t& fieldId) { uint32_t result = 0; int8_t type; result += readByte(type); fieldType = (TType)type; if (fieldType == T_STOP) { fieldId = 0; return result; } result += readI16(fieldId); return result; }
        
        uint32_t TBinaryProtocol::readFieldEnd() { return 0; }
        
        uint32_t TBinaryProtocol::readMapBegin(TType& keyType, TType& valType, uint32_t& size) { int8_t k, v; uint32_t result = 0; int32_t sizei; result += readByte(k); keyType = (TType)k; result += readByte(v); valType = (TType)v; result += readI32(sizei); if (sizei &lt; 0) { throw TProtocolException(TProtocolException::NEGATIVE_SIZE); } else if (container_limit_ && sizei &gt; container_limit_) { throw TProtocolException(TProtocolException::SIZE_LIMIT); } size = (uint32_t)sizei; return result; }
        
        uint32_t TBinaryProtocol::readMapEnd() { return 0; } } // apache::thrift::protocol 
然后来看看skip这个过程,其实这个过程和读的效果是一样的,只不过忽略值。这里看看如何skip一个struct的代码片断
case T_STRUCT: { uint32_t result = 0; std::string name; int16_t fid; TType ftype; result += readStructBegin(name); while (true) { result += readFieldBegin(name, ftype, fid); if (ftype == T_STOP) { break; } result += skip(ftype); result += readFieldEnd(); } result += readStructEnd(); return result; } 

驱动程序层面

考虑到市面上直接使用Thrift的Protocol比较少,没有什么这方面的例子,这里就写一个。

#include "x_types.h" #include #include #include #include <iostream> #include <cstdio> #include typedef ::apache::thrift::transport::TMemoryBuffer TMemoryBuffer; typedef ::apache::thrift::protocol::TBinaryProtocol TBinaryProtocol; typedef ::apache::thrift::protocol::TJSONProtocol TJSONProtocol;

uint32_t pack(unsigned char *buf,uint32_t size) { PACK pack; //do something about pack.. //并且我们需要显示设置... pack.a=1; pack.b=10; pack.c=12; pack.__isset.a=true; pack.__isset.b=true; pack.__isset.c=true; TMemoryBuffer *buffer=new TMemoryBuffer(); boost::shared_ptr<TMemoryBuffer> pbuffer(buffer); TBinaryProtocol prot(pbuffer); uint32_t wsize=pack.write(&prot); std::cout&lt;&lt;"write:"&lt;&lt;wsize&lt;&lt;" bytes"&lt;&lt;std::endl; if(wsize&gt;size){ fprintf(stderr,"not enough space\n"); } //这个类就是需要一次内存copy.. //因为内部也有autobuffer这样的机制.. uint32_t wsize1; unsigned char *buf1; pbuffer-&gt;getBuffer(&buf1,&wsize1); assert(wsize==wsize1); std::memcpy(buf,buf1,wsize); return wsize; }
        
        void unpack(unsigned char *buf,uint32_t size) { TMemoryBuffer *buffer=new TMemoryBuffer(buf,size); boost::shared_ptr<TMemoryBuffer> pbuffer(buffer); TBinaryProtocol prot(pbuffer); PACK pack; uint32_t rsize=pack.read(&prot); std::cout&lt;&lt;"read:"&lt;&lt;rsize&lt;&lt;" bytes"&lt;&lt;std::endl; assert(pack.a==1); assert(pack.b==10); assert(pack.c==12); return ; }
        
        int main() { unsigned char buf[1024]; uint32_t size=pack(buf,sizeof(buf)); unpack(buf,sizeof(buf)); return 0; }

参考列表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值