MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明

2017-12-30 12:13:20来源:oschina作者:xiaomin0322人点击

分享
MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明 博客分类: java


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明

第1部分messagepack说明 1.1messagepack的消息编码说明

为什么messagepack比json序列化使用的字节流更少,可通过图1-1、图1-2有个直观的感觉。

MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图1-1messagepack与json的格式对比1


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图1-2messagepack与json的格式对比2


messagepack的具体的消息格式如图1-3所示,messagepack的数据类型主要分类两类:固定长度类型和可变长度类型。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图1-3messagepack的消息格式


messagepack的具体类型信息表示如图1-4所示。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图1-4messagepack的类型信息

1.2messagepack的序列化和反序列化方式

现在msgpack能支持基本的数据类型,支持list和map,还支持自定义的数据类型。例子1,序列化和反序列化一个javabean,只要加上@MessagePackMessage的注解。



/**
* 一个用于messagepack测试序列化和反序列的javabean
*
* @author jimmee
*/
@MessagePackMessage
public class Person {
/** 编号 */
public int id;
/** 名字 */
public String name;
/**身高*/
public double height;
/**
* 默认构造函数
*/
public Person() {
}




序列化直接调用MessagePack的pack方法;反序列化则调用对应的unpack方法。这两个方法,都支持传递序列化和反序列化的数据类型。

1.3与json的序列化性能对比

如下所示,通过100条数据的序列化和反序列化进行对比。



List msgs = new ArrayList();
for (int i = 0; i < 100; i++) {
Map msg = new HashMap();
msg.put(Const.FID, i);
msg.put(Const.SUBJECT, "subject" + i);
msg.put(Const.LABEL0, 1);
msg.put(Const.FROM, "test@163.com");
msg.put(Const.TO, "test@126.com");
msg.put(Const.MODIFIED_DATE, new Date().getTime());
msg.put(Const.RECEIVED_DATE, new Date().getTime());
msg.put(Const.SENT_DATE, new Date().getTime());
msgs.add(msg);
}



比较结果如表1-1所示。


表1-1messagepack与json的性能对比


框架


字节大小(byte)


序列化时间(ns)


反序列化时间(ns)

messagepack


12793


2313335


529458

json


17181


1338371


1776519



可以看出,messagepack的序列化字节数比json小将近30%;序列化时间messagepack差不多是json的两倍;反序列化时间,messagepack只需要json的30%的时间。


但是,值得注意的是,虽然messagepack的反序列化时间比较少,但是要真正转换为前端需要的类型参数格式,还需要额外的一些时间。

第2部分protocolbuffers 2.1protocolbuffers的消息编码说明

ProtocolBuffers支持的数据类型如下图所示:


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图2-1protocolbuffers支持的数据类型。


首先对Varint进行说明。Varint是一种紧凑的表示数字的方法。它用一个或多个字节来示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。


比如对于int32类型的数字,一般需要4个byte来表示。但是采用Varint,对于很小的int32类型的数字,则可以用1个byte来表示。当然,采用Varint表示法,大的数字则需要5个byte来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用Varint后,可以用更少的字节数来表示数字信息。


Varint中的每个byte的最高位bit有特殊的含义,如果该位为1,表示后续的byte也是该数字的一部分,如果该位为0,则结束。其他的7个bit都用来表示数字。因此小于128的数字都可以用一个byte表示。大于128的数字,比如300,会用两个字节来表示:1010110000000010。


图2-2说明了GoogleProtocolBuffer如何解析两个bytes。注意到最终计算前将两个byte的位置相互交换过一次,这是因为GoogleProtocolBuffer字节序采用little-endian的方式。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图2-2protocolbuffers解析两个字节


消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的Key-Value对,如图2-3所示。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图2-3protocolbuffers的消息流


采用这种Key-Pair结构无需使用分隔符来分割不同的Field。对于可选的Field,如果消息中不存在该field,那么在最终的MessageBuffer中就没有该field,这些特性都有助于节约消息本身的大小。


假设我们生成如下的一个消息Message:


Message.id=5;

Message.info=“hello”;


则最终的MessageBuffer中有两个Key-Value对,一个对应消息中的id;另一个对应info。


Key用来标识具体的field,在解包的时候,ProtocolBuffer根据Key就可以知道相应的Value应该对应于消息中的哪一个field。


Key的定义如下:


(field_number<<3)|wire_type


可以看到Key由两部分组成。第一部分是field_number。第二部分为wire_type。表示Value的传输类型。


wiretype如表2-1所示。


表2-1wiretype说明


Type


Meaning


UsedFor

0


Varint


int32,int64,uint32,uint64,sint32,sint64,bool,enum

1


64-bit


fixed64,sfixed64,double

2


Length-delimited


string,bytes,embeddedmessages,packedrepeatedfields

3


Startgroup


Groups(deprecated)

4


Endgroup


Groups(deprecated)

5


32-bit


fixed32,sfixed32,float



在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用Varint表示一个负数,那么一定需要5个byte。为此GoogleProtocolBuffer定义了sint32,sint64类型,采用zigzag编码。


Zigzag编码用无符号数来表示有符号数字,正数和负数交错,如图2-3所示。使用zigzag编码,绝对值小的数字,无论正负都可以采用较少的byte来表示,充分利用了Varint这种技术。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图2-4ZigZag编码

2.2protocolbuffers的序列化和反序列化

步骤:


创建消息的定义文件.proto;


使用protoc工具将proto文件转换为相应语言的源码;


使用类库支持的序列化和反序列化方法进行操作。



以同样的数据的操作为例:


1.定义proto文件messages.ptoto



message MessageMeta {
required int32 id = 1;
required string subject = 2;
optional int32 lablel0 = 3;
required string from = 4;
required string to = 5;
optional int64 modifiedDate = 6;
optional int64 receivedDate = 7;
optional int64 sentDate = 8;
}





message MessageMetas {
repeated MessageMeta msg = 1;
}



2.将message.proto文件转换为java语言的源码


例如,执行命令:protoc-I=src--java_out=outsrc/messages.proto产生Messages的java文件。


3.执行序列化和反序列化



MessageMetas.Builder msgsBuilder = MessageMetas.newBuilder();
for (int i = 0; i < 100; i++) {
MessageMeta.Builder msgBuilder = MessageMeta.newBuilder();
msgBuilder.setId(i);
msgBuilder.setSubject("subject" + i);
msgBuilder.setLablel0(1);
msgBuilder.setFrom("test@163.com");
msgBuilder.setTo("test@126.com");
msgBuilder.setModifiedDate(new Date().getTime());
msgBuilder.setReceivedDate(new Date().getTime());
msgBuilder.setSentDate(new Date().getTime());
msgsBuilder.addMsg(msgBuilder.build());
}
MessageMetas msgs = msgsBuilder.build();



之后调用相应的writeTo方法进行序列化,调用parseFrom进行反序列化。

2.3与json等的性能对比

表2-2性能对比表格


框架


字节大小(byte)


序列化时间(ns)


反序列化时间(ns)

messagepack


12793


2313335


529458

protocolbuffers


6590


941790


408571

json


17181


1338371


1776519



可以看出,protocolbuffers在字节流,序列化时间和反序列化时间方面都明显较优(即空间和时间上都比较好)。

第3部分thrift

thrift的架构如图3-1所示。图3-1显示了创建server和client的stack。最上面的是IDL,然后生成Client和Processor。红色的是发送的数据。protocol和transport是Thrift运行库的一部分。通过Thrift你只需要关心服务的定义,而不需要关心protocol和transport。


Thrift支持text和binaryprotocols,binaryprotocols要比textprotocols,但是有时候textprotocols比较有用(例如:调试的时候)。支持的协议有:


TBinaryProtocol:直接的二进制格式


TCompactProtocol:效率和高压缩编码数据


TDenseProtocoal:和TCompactProtocol相似,但是省略了meta信息,从哪里发送的,增加了receiver。还在实验中,java实现还不可用。


TJSONProtocoal:使用JSON


TSImpleJSONProtocoal:只写的protocol使用JSON。适合被脚本语言转化


TDebugProtocoal:使用人类可读的text格式帮助调试


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图3-1thrift架构图


上面的protocol说明了传送的是什么样的数据,Thrift的transports则说明了怎样传送这些数据。支持的transport:


TSocket:使用blockingsocketI/O;


TFramedTransport:以帧的形式发送,每帧前面是一个长度。要求服务器来non-blockingserver;


TFileTransport:写到文件;


TMemoryTransport:使用内存I/O,java实现中在内部使用了ByteArrayOutputStream;


TZlibTransport压缩使用zlib,在java实现中还不可用。


最后,thrift提供了servers:


TSimpleServer:单线程server,使用标准的blockingIO,用于测试;


TThreadPoolServer:多线程server,使用标准的blockingIO;


TNonblockingServer多线程server,使用non-blockingIO(java实现中使用了NIOchannels),TFramedTransport必须使用在这个服务器。


一个server只允许定义一个接口服务。这样的话多个接口需要多个server。这样会带来资源的浪费。通常可以通过定义一个组合服务来解决。

3.1thrift的消息编码说明

1.支持的数据类型


所有编程语言中都可用的关键类型。


bool布尔值,真或假


byte有符号字节


i1616位有符号整数


i3232位有符号整数


i6464位有符号整数


double64位浮点数


string与编码无关的文本或二进制字符串


可基于基本类型定义结构体,例如:



struct Example {
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name="thrifty"
}



支持的容器有list,set和Map


若使用TCompactProtocol,传递的消息形式如图3-2所示:


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图3-2thrift的compact方式的消息流


在这种方式下,对整数而言,也是采用可变长度的方式进行实现。一个字节,最高位表示是否还有数据,低7位是实际的数据,如图3-3所示,整数106903的编码,相比普通的int类型,节省一个字节。


MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明


图3-3compact方式对一个整数106903进行编码

3.2thrift的序列化和反序列化方式

步骤:


创建thrift接口定义文件;


将thrift的定义文件转换为对应语言的源代码;


选择相应的protocol,进行序列化和反序列化。


仍以同样的数据对象为例子:


定义thrift文件messages.thrift



struct MessageMeta {
1:i32 id;
2:string subject;
3:i32 lablel0;
4:string from;
5:string to;
6:i64 modifiedDate;
7:i64 receivedDate;
8:i64 sentDate;
}struct MessageMetas {
1:list msgs;
}




2.将定义的文件转换成相应的java源码


执行命令:thrift-genjavamessages.thrift


3.执行序列化和反序列化



MessageMetas msgs = new MessageMetas();
List msgList = new ArrayList();
for (int i = 0; i < 100; i++) {
MessageMeta msg = new MessageMeta();
msg.setId(i);
msg.setSubject("subject" + i);
msg.setLablel0(1);
msg.setFrom("test@163.com");
msg.setTo("test@126.com");
msg.setModifiedDate(new Date().getTime());
msg.setReceivedDate(new Date().getTime());
msg.setSentDate(new Date().getTime());
msgList.add(msg);
}
msgs.setMsgs(msgList);
// 序列化
ByteArrayOutputStream out = new ByteArrayOutputStream();
TTransport trans = new TIOStreamTransport(out);
TBinaryProtocol tp = new TBinaryProtocol(trans);
msgs.write(tp);byte [] buf = out.toByteArray();
// 反序列化
ByteArrayInputStream in = new ByteArrayInputStream(buf);
trans = new TIOStreamTransport(in);
tp = new TBinaryProtocol(trans);
MessageMetas msgs2 = new MessageMetas();
msgs2.read(tp);


3.3与json等的性能对比

表3-1性能对比


框架


字节大小(byte)


序列化时间(ns)


反序列化时间(ns)

messagepack


12793


2313335


529458

protocolbuffers


6590


941790


408571

thrift


6530


798696


754458

json


17181


1338371


1776519



通过对比,可以发现thrift总的来说,都比较不错。

第4部分小结

通过对messagepack,protocolbuffers以及thrift的分析,主要分析了这些框架的序列化和反序列化部分的内容。实际上messagepack和thrift都还有自己的rpc调用框架。


所有的测试都是在本机上进行,基于100条元数据进行测试。可能不同数据,以及不同的规模,测试结果应该会存在差别,https://github.com/eishay/jvm-serializers/wiki/的有比较好的测试结果说明。根据自己的测试,从性能上说,messagepack,protocolbuffers以及thrift都比json好(在测试时,发现messagepack序列化的时间稍微多一些)。


从编程语言上来说,messagepack,protocolbuffers以及thrift,当然还包括json,都是支持跨语言的通讯的。


从接口定义的灵活性来(或者是否支持动态类型),messagepack较protocolbuffers以及thrift较好,后两者都要预先定义schema并相对固定。



实际工作中, 一般都采用protocol buffers或者thrift.


第5部分参考资料

1.http://msgpack.org/


2.http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/overview.html


3.http://jnb.ociweb.com/jnb/jnbJun2009.html


4.http://code.google.com/p/thrift-protobuf-compare/


5.http://www.tbdata.org/archives/1307


6.https://github.com/eishay/jvm-serializers/wiki/


7.http://wiki.apache.org/thrift/


8.http://pypi.python.org/pypi/msgpack-python/



http://www.open-open.com/lib/view/open1412731170858.html

微信扫一扫

第七城市微信公众平台