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

时间:2023-01-10 15:00:05

第1部分 messagepack说明

1.1messagepack的消息编码说明

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

 

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


 

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

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


 

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

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

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


 

图1- 3的消息格式

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

 

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


 

图1- 4的类型信息

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

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

 

Java代码 ​

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

    1. /**
    2. * 一个用于messagepack测试序列化和反序列的javabean
    3. *
    4. * @author jimmee
    5. */
    6. @MessagePackMessage
    7. public class
    8. /** 编号 */
    9. public int
    10. /** 名字 */
    11. public
    12. /**身高*/
    13. public double
    14. /**
    15. * 默认构造函数
    16. */
    17. public
    18. }


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

     

     

     

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

    1.3json的序列化性能对比

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

     

    Java代码 ​​​​ 

      1. List<Map> msgs = new
      2. for (int i = 0; i < 100; i++) {
      3. Map msg = new
      4. msg.put(Const.FID, i);
      5. msg.put(Const.SUBJECT, "subject"
      6. msg.put(Const.LABEL0, 1);
      7. msg.put(Const.FROM, "test@163.com");
      8. msg.put(Const.TO, "test@126.com");
      9. msg.put(Const.MODIFIED_DATE, new
      10. msg.put(Const.RECEIVED_DATE, new
      11. msg.put(Const.SENT_DATE, new
      12. msgs.add(msg);
      13. }

      List<Map> msgs = new ArrayList<Map>();
      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- 1与json的性能对比

      框架

      字节大小(byte)

      序列化时间(ns)

      反序列化时间(ns)

      messagepack

      12793

      2313335

      529458

      json

      17181

       1338371

      1776519

       

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

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

      第2部分 protocol buffers

      2.1的消息编码说明

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

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


        

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

      首先对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,会用两个字节来表示:1010 1100 0000 0010。

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

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


        

      图2- 2解析两个字节

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

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


        

      图2- 3的消息流

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

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

      Message.id = 5; 
      Message.info;

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

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

      Key 的定义如下:

      (field_number << 3) | wire_type

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

      wire type如表2-1所示。

      表2- 1说明

      Type 

      Meaning 

      Used For 

      Varint 

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

      64-bit 

      fixed64, sfixed64, double 

      Length-delimited 

      string, bytes, embedded messages, packed repeated fields 

      Start group 

      Groups (deprecated) 

      End group 

      Groups (deprecated) 

      32-bit 

      fixed32, sfixed32, float 

       

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

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

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

        

      图2- 4编码

      2.2的序列化和反序列化

      步骤:

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

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

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

       

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

      1. 定义proto文件messages.ptoto

       

      Java代码 ​

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

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

      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;
      }

       

       

       

       

      Java代码 ​

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

      1. message MessageMetas {  
      2. repeated MessageMeta msg = 1;
      3. }

      message MessageMetas {
      repeated MessageMeta msg = 1;
      }

       

       

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

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

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

       

      Java代码 ​

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

        1. MessageMetas.Builder msgsBuilder = MessageMetas.newBuilder();  
        2. for (int i = 0; i < 100; i++) {
        3. MessageMeta.Builder msgBuilder = MessageMeta.newBuilder();
        4. msgBuilder.setId(i);
        5. msgBuilder.setSubject("subject"
        6. msgBuilder.setLablel0(1);
        7. msgBuilder.setFrom("test@163.com");
        8. msgBuilder.setTo("test@126.com");
        9. msgBuilder.setModifiedDate(new
        10. msgBuilder.setReceivedDate(new
        11. msgBuilder.setSentDate(new
        12. msgsBuilder.addMsg(msgBuilder.build());
        13. }
        14. MessageMetas msgs = msgsBuilder.build();

        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.3json等的性能对比

        表2- 2 性能对比表格

        框架

        字节大小(byte)

        序列化时间(ns)

        反序列化时间(ns)

        messagepack

        12793

        2313335

        529458

        protocol buffers

        6590

        941790

        408571

        json

        17181

         1338371

        1776519

         

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

        第3部分 thrift

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

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

        TBinaryProtocol :直接的二进制格式

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

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

        TJSONProtocoal:使用JSON

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

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

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


          

        图3- 1架构图

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

        TSocket :使用 blocking socket I/O;

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

        TFileTransport :写到文件;

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

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

        最后,thrift 提供了servers:

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

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

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

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

        3.1的消息编码说明

        1. 支持的数据类型

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

        bool 布尔值,真或假

        byte 有符号字节

        i16  16位有符号整数

        i32  32位有符号整数

        i64  64位有符号整数

        double 64位浮点数

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

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

         

        Java代码 ​

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

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

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

         

         

        支持的容器有list<type>,set<type>和Map<type1,type2>。

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

         

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


         

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

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

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


          

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

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

        步骤:

        创建thrift接口定义文件;

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

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

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

        定义thrift文件messages.thrift

         

        Java代码 ​​​​ 

          1. struct MessageMeta {  
          2. 1:i32 id;
          3. 2:string subject;
          4. 3:i32 lablel0;
          5. 4:string from;
          6. 5:string to;
          7. 6:i64 modifiedDate;
          8. 7:i64 receivedDate;
          9. 8:i64 sentDate;
          10. }
          11.
          12. struct MessageMetas {
          13. 1:list<MessageMeta> msgs;
          14. }


          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<MessageMeta> msgs;
          }

           

           

           

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

          执行命令:thrift -gen java messages.thrift

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

           

          Java代码 ​​​​ 

            1. MessageMetas msgs = new
            2. List<MessageMeta> msgList = new
            3. for (int i = 0; i < 100; i++) {
            4. MessageMeta msg = new
            5. msg.setId(i);
            6. msg.setSubject("subject"
            7. msg.setLablel0(1);
            8. msg.setFrom("test@163.com");
            9. msg.setTo("test@126.com");
            10. msg.setModifiedDate(new
            11. msg.setReceivedDate(new
            12. msg.setSentDate(new
            13. msgList.add(msg);
            14. }
            15. msgs.setMsgs(msgList);
            16. // 序列化
            17. ByteArrayOutputStream out = new
            18. TTransport trans = new
            19. TBinaryProtocol tp = new
            20. msgs.write(tp);
            21.
            22. byte
            23. // 反序列化
            24. ByteArrayInputStream in = new
            25. trans = new
            26. tp = new
            27. MessageMetas msgs2 = new
            28. msgs2.read(tp);


            MessageMetas msgs = new MessageMetas();
            List<MessageMeta> msgList = new ArrayList<MessageMeta>();
            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

            protocol buffers

            6590

            941790

            408571

            thrift

            6530

            798696

            754458

            json

            17181

             1338371

            1776519

             

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

            第4部分 小结

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

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

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

            从接口定义的灵活性来(或者是否支持动态类型),messagepack较protocol buffers以及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/​