thrift序列化 java_[thrift]thrift中的对象序列化

本文炒冷饭.说实话,一直挺看好Thrift,支持的语言又多,代码写的有很清晰,效率又不低,为啥研究Protocol Buffer的人那么多.不管那么多了....

Thrift中的对象序列化是我很看好的东西,他用compiler+类库,让你高效的完成任务,而且可以少犯错误.试想,有谁可以保证自己设计的对象,不会再改变呢?数据库的schema改了,你可以改改查询语句,但是如果你对象改了,之前序列化好的东西,有时候就很难搞回来了.(哎.....)

废话不说,看Thrift里面怎么搞的.

1. Thrift支持的数据类型

Thrift支持的数据类型定义在TProtocol.h这个头文件中,有一个TType的枚举:

enumTType {

T_STOP=0,

T_VOID=1,

T_BOOL=2,

T_BYTE=3,

T_I08=3,

T_I16=6,

T_I32=8,

T_U64=9,

T_I64=10,

T_DOUBLE=4,

T_STRING=11,

T_UTF7=11,

T_STRUCT=12,

T_MAP=13,

T_SET=14,

T_LIST=15,

T_UTF8=16,

T_UTF16=17};

而每一种Protocol都不一定全部支持这么多数据格式,T_LIST之前的都是被支持的.T_STRING是c string,可以和utf-8兼容.

2. Thrift对各种数据类型的读写

Thrift把对象序列化抽象成TProtocol这样一个抽象类,这个类的成员非常多,但是思路很明显,就是对各种数据类型的读写操作:

classTProtocol {public:virtual~TProtocol() {}

uint32_t writeMessageBegin(conststd::string&name,constTMessageType messageType,constint32_t seqid);

uint32_t writeMessageEnd();

uint32_t writeFieldBegin(constchar*name,constTType fieldType,constint16_t fieldId) ;

uint32_t writeFieldEnd();

uint32_t writeFieldStop();//写各种类型的数据uint32_t writeBool(constboolvalue);

uint32_t writeByte(constint8_tbyte);

uint32_t writeI16(constint16_t i16);

uint32_t writeStructBegin(constchar*name);

uint32_t writeStructEnd();//此处省略若干行uint32_t readMessageBegin(std::string&name,

TMessageType&messageType,

int32_t&seqid);

uint32_t readMessageEnd();

uint32_t readFieldBegin(std::string&name,

TType&fieldType,

int16_t&fieldId);

uint32_t readFieldEnd() ;//读各种类型的数据uint32_t readBool(bool&value);

uint32_t readI16(int16_t&i16) ;

uint32_t readStructBegin(std::string&name) ;

uint32_t readStructEnd();//此处省略若干行};

每一种数据类型都一对read/write方法,另外Message,Field也有read/write方法.

网友可能很奇怪,为啥抽象类的方法不是虚的.......其实我这里代码省略了很多,之前0.5.0版本的thrift里面,这些方法都是虚的,对TProtocol的实现都重写了这些方法;0.6.0里面,直接的read/write方法都不是虚的,但是添加了额外的虚函数:

//比如说对list的读virtualuint32_t readListBegin_virt(TType&elemType,

uint32_t&size)=0;virtualuint32_t readListEnd_virt()=0;//另外有uint32_t readListBegin(TType&elemType, uint32_t&size) {

T_VIRTUAL_CALL();returnreadListBegin_virt(elemType, size);

}

uint32_t readListEnd() {

T_VIRTUAL_CALL();returnreadListEnd_virt();

}

其实和直接使用虚函数是一样的.

OK,接口看完了,这就去看对接口的实现,我们来看TBinaryProtocolT是怎么实现的.这里多说几句,Thrift里面实现了好多种序列化,如果你觉得这种序列化不好,可以去重新实现一个上面说的那个接口,就可以工作了:-D

TBinaryProtocolT里面我们看一两个具有代表性的,int32_t/map的读写:

templateuint32_t TBinaryProtocolT::writeI32(constint32_t i32) {//把数字转化成网络字节序int32_t net=(int32_t)htonl(i32);//然后写入到transportthis->trans_->write((uint8_t*)&net,4);//返回写入数据的大小return4;

}

templateuint32_t TBinaryProtocolT::readI32(int32_t&i32) {

uint8_t b[4];this->trans_->readAll(b,4);

i32=*(int32_t*)b;//读取四个字节,转为本地字节序i32=(int32_t)ntohl(i32);//返回读出数据的大小return4;

}

templateuint32_t TBinaryProtocolT::writeMapBegin(constTType keyType,constTType valType,constuint32_t size) {

uint32_t wsize=0;//写入一个byte的key类型TTypewsize+=writeByte((int8_t)keyType);//写入一个byte的value类型TTypewsize+=writeByte((int8_t)valType);//再写入元素的个数,int32_t的wsize+=writeI32((int32_t)size);returnwsize;

}

templateuint32_t TBinaryProtocolT::readMapBegin(TType&keyType,

TType&valType,

uint32_t&size) {

int8_t k, v;

uint32_t result=0;

int32_t sizei;//读的时候也是类似,读取key的类型,value的类型,还有元素的个数result+=readByte(k);

keyType=(TType)k;

result+=readByte(v);

valType=(TType)v;

result+=readI32(sizei);if(sizei<0) {throwTProtocolException(TProtocolException::NEGATIVE_SIZE);

}elseif(this->container_limit_&&sizei>this->container_limit_) {throwTProtocolException(TProtocolException::SIZE_LIMIT);

}

size=(uint32_t)sizei;returnresult;

}

可以看到,代码内聚很强,也很易懂.float/double的序列化是通过强转成uint32_t/uint64_t来实现的,string么,先去写一个大小,然后才是内容.list和set都是类似的~~

对于field的read/write都是直接写该filed的类型信息,而field那么就忽略掉了,因为二进制序列化用不到那些东西,只有json这样的文本序列化才能用到:-D,有兴趣的可以去看看JSON Protocol的实现

3. 代码生成

如果让你手写对象的序列化,反序列化,你肯定要抱怨了,因为那样出错的机会非常大.Thrift和Protocol Buffer都给你提供了编译器,写好IDL之后,可以用编译器生成好代码~~这样可以保证不会出错.以UserProfile为例:

structUserProfile {1: i32 uid,2:stringname,3:stringblurb

}

生成代码: thrift-0.6.0.exe --gen cpp UserProfile.thrift

这样,thrift会在gen-cpp文件夹内生成好UserProfile的代码,我们只想看UserProfile类的read和write是怎么实现的:

uint32_t UserProfile::write(::apache::thrift::protocol::TProtocol*oprot)const{

uint32_t xfer=0;//write的代码比较简单,就是按照顺序//把field的类型和fieldid和value写进去xfer+=oprot->writeStructBegin("UserProfile");

xfer+=oprot->writeFieldBegin("uid", ::apache::thrift::protocol::T_I32,1);

xfer+=oprot->writeI32(this->uid);

xfer+=oprot->writeFieldEnd();

xfer+=oprot->writeFieldBegin("name", ::apache::thrift::protocol::T_STRING,2);

xfer+=oprot->writeString(this->name);

xfer+=oprot->writeFieldEnd();

xfer+=oprot->writeFieldBegin("blurb", ::apache::thrift::protocol::T_STRING,3);

xfer+=oprot->writeString(this->blurb);

xfer+=oprot->writeFieldEnd();

xfer+=oprot->writeFieldStop();

xfer+=oprot->writeStructEnd();returnxfer;

}

uint32_t UserProfile::read(::apache::thrift::protocol::TProtocol*iprot) {

uint32_t xfer=0;

std::stringfname;

::apache::thrift::protocol::TType ftype;

int16_t fid;

xfer+=iprot->readStructBegin(fname);using::apache::thrift::protocol::TProtocolException;while(true)

{//read的时候,每次都是先读出来field的类型和fieldidxfer+=iprot->readFieldBegin(fname, ftype, fid);if(ftype==::apache::thrift::protocol::T_STOP) {break;

}switch(fid)

{//然后查看当前反序列化的fieldid和读出来的field是不是同一个类型的//如果是就反序列化//不是就skip....//类型就是靠之前说的TTypecase1:if(ftype==::apache::thrift::protocol::T_I32) {

xfer+=iprot->readI32(this->uid);this->__isset.uid=true;

}else{

xfer+=iprot->skip(ftype);

}break;case2:if(ftype==::apache::thrift::protocol::T_STRING) {

xfer+=iprot->readString(this->name);this->__isset.name=true;

}else{

xfer+=iprot->skip(ftype);

}break;case3:if(ftype==::apache::thrift::protocol::T_STRING) {

xfer+=iprot->readString(this->blurb);this->__isset.blurb=true;

}else{

xfer+=iprot->skip(ftype);

}break;default:

xfer+=iprot->skip(ftype);break;

}

xfer+=iprot->readFieldEnd();

}

xfer+=iprot->readStructEnd();returnxfer;

}

使用这个类也是比较简单的,Thrift的transport对读写操作做了一定的抽象,你可以读写网络端口,文件,内存等,我们这里用内存:

typedef unsignedlonguint32_t;

typedef unsignedcharuint8_t;

uint32_t bufferSize=64*1024;

uint8_t*buffer=(uint8_t*)malloc(bufferSize);

boost::shared_ptr_write(newTMemoryBuffer(buffer,bufferSize,TMemoryBuffer::TAKE_OWNERSHIP));

TProtocol*protowrite=newTBinaryProtocol(_write);

UserProfile _userProfile;//在这里修改_userProfile的属性//....//序列化uint32_t writeSize=_userProfile.write(protowrite);

boost::shared_ptr_read(newTMemoryBuffer(buffer,bufferSize));

TProtocol*protoread=newTBinaryProtocol(_read);

UserProfile _userProfile2;

_userProfile2.read(protoread);

asser(_userProfile==_userProfile2);

很ez吧

4. Thrift向后兼容性的实现

看生成好的read代码可以看到有一个skip的方法(当他类型不一样的时候),这个就是向后兼容性实现的关键.一个对象,难免被改来改去,改不要紧,field id要变化,不要一个field id用到死..当然你继续用也没问题,比如你之前field id 1 type int32_t,之后还是这样,读是读出来了,有可能逻辑不对了.....来看看skip的实现.

//策略就是,读到什么抛弃什么templateuint32_t skip(Protocol_&prot, TType type)

{switch(type) {caseT_BOOL:

{boolboolv;returnprot.readBool(boolv);

}caseT_BYTE:

{

int8_t bytev;returnprot.readByte(bytev);

}//此处省略若干行//因为代码都是类似的//对结构体,map/list/set之类的skip操作是比较复杂的caseT_STRUCT:

{

uint32_t result=0;

std::stringname;

int16_t fid;

TType ftype;

result+=prot.readStructBegin(name);while(true) {

result+=prot.readFieldBegin(name, ftype, fid);if(ftype==T_STOP) {break;

}

result+=skip(prot, ftype);

result+=prot.readFieldEnd();

}

result+=prot.readStructEnd();returnresult;

}

}

OK,至此,Thrift对象序列化的代码基本上就看的差不多了.因为我们不会去用Thrift的Service,所以那部分代码没看过....

Thrift千好万好,但是如果你数据写好了,schema丢失了.....那就不好玩了

PS:很早之前看过Thrift,但是没写篇文章总结一下.这篇也算是了了心愿.