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, }
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;
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; };
问题
- 没有使用接口和进行字段的设置和获取,在一定程度上简化了访问。但是使得在填写字段和获取字段的时候需要自己手动去访问一下__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->readStructBegin(fname); using ::apache::thrift::protocol::TProtocolException; while (true) { //对于每一个字段,首先读取field begin //读取fname["SUB"],ftype[这个字段是什么类型],fid[字段号] xfer += iprot->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->readI16(this->a_555); this->__isset.a_555 = true; } else { //如果字段号和类型不匹配的话,那么会忽略这个长度 xfer += iprot->skip(ftype); } break; case 2: if (ftype == ::apache::thrift::protocol::T_I16) { xfer += iprot->readI16(this->b_555); this->__isset.b_555 = true; } else { xfer += iprot->skip(ftype); } break; default: xfer += iprot->skip(ftype); break; } //然后会读取一个field end xfer += iprot->readFieldEnd(); } //最后读取一个struct end xfer += iprot->readStructEnd(); return xfer; } uint32_t SUB::write(::apache::thrift::protocol::TProtocol* oprot) const { uint32_t xfer = 0; //首先先写一个struct begin,配上"SUB"[class name] xfer += oprot->writeStructBegin("SUB"); //对于可选字段的话那么会判断可选字段是否设置 if (this->__isset.a_555) { xfer += oprot->writeFieldBegin("a_555", ::apache::thrift::protocol::T_I16, 1); xfer += oprot->writeI16(this->a_555); xfer += oprot->writeFieldEnd(); } //开始写字段,field_name,ftype,fid.field_name可能会出现在output中,对于binary不需要,json就需要 xfer += oprot->writeFieldBegin("b_555", ::apache::thrift::protocol::T_I16, 2); xfer += oprot->writeI16(this->b_555); xfer += oprot->writeFieldEnd(); xfer += oprot->writeFieldStop(); xfer += oprot->writeStructEnd(); return xfer; }
- read_struct_begin
- read_field_begin(fname, ftype,fid)
- 如果fid和ftype匹配的话,那么就会读取数据并且置位,否则跳过ftype长度
- read_field_end
- read_struct_end
- write_struct_begin(fname)
- write_field_begin(fname,ftype,fid)
- 如果可选的话那么会看是否设置,然后write出去
- write_field_end
- write_struct_stop并且write_struct_end
if (ftype == ::apache::thrift::protocol::T_MAP) { this->d.clear(); uint32_t _size9; ::apache::thrift::protocol::TType _ktype10; ::apache::thrift::protocol::TType _vtype11; //读取k,vtype,和size.. //read_map_begin iprot->readMapBegin(_ktype10, _vtype11, _size9); uint32_t _i13; for (_i13 = 0; _i13 < _size9; ++_i13) { SUB _key14; xfer += _key14.read(iprot); SUB& _val15 = this->d[_key14]; xfer += _val15.read(iprot); } //read_map_end.. iprot->readMapEnd(); this->__isset.d = true; } else { xfer += iprot->skip(ftype); }
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_->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 > 0) { trans_->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_->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); }
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 < 0) { throw TProtocolException(TProtocolException::NEGATIVE_SIZE); } else if (container_limit_ && sizei > container_limit_) { throw TProtocolException(TProtocolException::SIZE_LIMIT); } size = (uint32_t)sizei; return result; } uint32_t TBinaryProtocol::readMapEnd() { return 0; } } // apache::thrift::protocol
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<<"write:"<<wsize<<" bytes"<<std::endl; if(wsize>size){ fprintf(stderr,"not enough space\n"); } //这个类就是需要一次内存copy.. //因为内部也有autobuffer这样的机制.. uint32_t wsize1; unsigned char *buf1; pbuffer->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<<"read:"<<rsize<<" bytes"<<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; }