移动场景下通信协议FlatBuffers、ProtocolBuffers、MessagePack选优

大家都知道JSON是纯文本协议,优点是可读性高,使用简单方便;而正是它的优点造成了它解析费时、解析内存耗费高、及数据量大的问题。在移动场景对性能要求极高的情况下,选择JSON作为通信协议无疑不是最佳。为了解决上述问题,特对MessagePack、FlatBuffer、ProtocolBuffers这几种当下流行的通信协议进行了学习研究,与JSON相对比,综合不同场景分析协议的优劣之处。
首先简单介绍一下几种协议的基本原理及其用法。
FlatBuffers,可参见FlatBuffers在android的使用简介android FlatBuffers剖析
MessagePack,参见MessagePack简介及使用
ProtoBuf,参见ProtocolBuffers的编码Android Studio使用ProtocolBuffers
网上有很多对这几种数据协议的对比和评论,但大都范范的给出FB更好或者是PB更优的结论,我想说这是很不严谨的,不同的数据格式、真实数据的内容、数据的复杂度等都影响了几种协议的对比效果。而实际使用场景中也必须从序列化反序列化速度、内存耗费、使用成本等方面综合考虑。
下面就从几大方面选取不同数据样本进行分析对比,希望能给大家一个参考。

第一种对比方式,改变样本量大小看对比结果

样本1数据样式

{
  "error_no":0,
  "message":"",
  "result":{
    "data":[
      {
        "datatype":1,
        "itemdata":
            {//共有字段45个
              "sname":"\u5fae\u533b",
              "packageid":"330611",
              …
              "tabs":[
                        {
                          "type":1,
                          "f":"abc"
                        },
                        …
              ]
            }
      },
      …
    ],
    "hasNextPage":true,
    "dirtag":"soft"
  }
}

测试方式

通过填充data的数据个数来扩大样本容量,选取JSON格式下的6K(data数据=1)、30K(data数据=7)、60K(data数据=15)、600K(data数据=154)、6000K(data数据=1540)、12000K(data数据=3080)为样本。
从客户端角度,对比以下四方面:序列化后原始数据大小、反序列化耗时、反序列化耗内存大小。结果对比主要选择6K、30K、60K、600K、6000K的测试结果进行。

测试结论

1. 序列化后数据大小对比

相同的数据内容用不同格式编码,进行存储后的文件大小数据如下表:



序列化数据压缩对比图

按照节省的空间从大到小排序:msgpack> pb>fb>json。由于该数据样本中基本数据类型(int,float等)较少,因此pb对基本数据类型进一步zigzag编码的优势没有充分体现,但与msgpack数据相差不大。
排名:msgpack> pb>fb

2. 反序列化耗内存大小对比

相同的数据内容按不同格式编码后,读取到内存中,进行反序列化操作,查看占用的内存情况



反序列化耗费内存大小对比.png

反序列化耗费内存从大到小排序msgpack>json>pb>fb,msgpack耗费内存约为json的1.6倍,fb、pb耗费内存差别不大。
排名:fb>pb >msgpack

3. 反序列化耗时

将序列化后的数据读取到内存后,开始计算反序列化成完整java对象的时间



在解析耗时方面这几种数据格式都优于json,排名:pb>msgpack>fb。

4. 该对比方式下的结论:

在反序列化耗内存大小方面,msgpack远远超出json(1.6倍),由于内存是移动客户端场景下的重要指标,因此基本可以排除msgpack。
只看pb和fb的排名:
序列化后大小:pb>fb
反序列化耗内存: fb>pb
反序列化耗时:pb>fb
综合来看pb更优

第二种对比方式:考虑层级对结论的影响

排除了msgpack后,我们对pb和fb进行进一步分析,考虑样本对对比结果的影响,选取两种样本:

样本选取

样本2将层级内数据减少到最小

{
   "error_no":0,
   "message":"",
   "result":{
          "data":[
             {
                "datatype":1,
                "itemdata":
                            {
                              "sname":"微信",
                              "tabs":[
                                       {
                                        "type":1
                                        }
                                      ]
                             }
             },
             ……
          ],
       }
}

样本3层级为0,数据个数多

{//共有字段45个
"sname":"微信",
"packageid":"330611",
 … 
}

测试方式

选取样本2、样本3序列化后的数据分别进行1000次反序列化,对比总耗时和总耗费内存

测试结论

1. 序列化后数据大小对比

样本类型 FB(kb) PB(kb)
样本2 0.21 0.06
样本3 4.08 3.65

pb比fb序列化后更省空间,这个结论与上面结论相同,可见PB在不同样本下序列化后的内存大小更佳。

2. 反序列化耗内存大小对比

样本类型 FB(kb) PB(kb)
样本2 1086 943
样本3 7650 11160

数据量较大时FB比PB解析更省内存,层级较深时PB更省内存。

3. 反序列化耗时对比

样本类型 FB(ms) PB(ms)
样本2 415 546
样本3 1614 991

可见,当数据样式层级及结构复杂度较高,而层级内数据量不大时,反序列化耗时FB更有优势,但两者差别不大,当层级内数据量较大时,PB更省时间。

进一步结论,干货时间!!!

  1. 在序列化后的存储空间占用方面,PB绝对优于FB,假设样本中数字较多时,PB的优势将更明显,因为zigzag的编码让数字变得更省空间。
  2. 在解析耗时方面,PB更优。虽然当数据层级较深而层级内数据量较小时,FB更有优势,而一旦层级内数据量变大,则PB会更省时间。为什么PB在这种场景下更快呢?分析这可能与FB的寻址方式有关,每个变量的读取都需要先去vtable查询偏移量,再根据偏移量移动指针读取数据,而偏移量都是基于vtable基准点计算得出的,每次读取都需要从基准点处重新寻址,而且读取过程没有顺序可言,当层级内数据较多时全部读取完毕需要更长时间。而PB不需要这么复杂的过程,直接顺序读取完毕,按k-v的对应关系赋值给相应变量即可。因此,FB更适合存储大块二进制数据,比如图片、游戏数据等;PB更适合短而复杂的通信协议。
  3. 在解析耗内存方面,FB比PB更优。这也是由存储方式以及协议的复杂度决定的,FB在访问数据时不需要创建临时内存,每个数据的存储位置可以认为是指定好的,直接读取内存就可实现反序列化,不需要复杂的解析逻辑,因此省去了很多中间对象的创建、内存申请;而PB是K-V存储方式,每个数据存放的先后顺序及位置是不能预知的,因此需要遍历过程去完成数据的解析,然后包装对应到对象,比FB多了解析过程。

实际传输场景下的选优

笔者项目目前的通信协议基本为样式1所示数据格式,按分析结论选择PB为最佳,但在实际通信场景中是否如此呢,为此做了进一步验证。

测试方式

一次传输过程中数据传输前压缩(Gzip)和不压缩两种情况下,对比客户端总内存消耗和传输流量降低、传输耗时几方面。
基于样本1数据,取PB、FB与JSON进行对比。样本数据60K(Json格式下)。

1. 内存开销及传输流量对比

协议及压缩方式 原始数据 序列化 压缩后大小 传输大小 解压用内存 反序列化内存 客户端总内存耗费 内存开销降低 传输流量降低
JSON+gzip 60 60 14.22 14.22 200.41 473 687.63
PB+gzip 60 33 13.41 13.41 167.65 133 314.06 54% 6%
PB 60 33 33 0 133 166 76% -132%
FB+gzip 60 38.12 15.12 15.12 184.02 130 329.14 52% -6%
FB 60 38.12 38.12 0 130 168.12 76% -168%

结论:

  1. 不采用GZIP压缩直接进行二进制传输对内存开销降低影响明显,但反面影响是传输流量会增长1.3到1.7倍不等。考虑非WIFI用户的流量成本,建议仍然使用GZIP压缩的方式。
  2. FB+GZIP的传输流量JSON+GZIP方式还要多,且总内存耗费要高于PB+GZIP
    因此选择PB+GZIP。

2. 整体耗时

一次传输完成耗时=通信耗时+解压缩耗时+反序列化耗时,由于压缩后的数据大小相差并不明显,因此同是GZIP压缩后再传输对通信时间的影响可忽略不计。因此数据从传输开始到解析完成的耗时只受解压缩耗时和反序列化耗时两方面影响。
对比几种数据解压缩耗时结果如下表

Data个数 JSON FB PB
15 2.4 2.7 1.9
154 19.3 22.2 16
1540 173.9 180.8 157.8
3080 327.4 304.6 237.9

结论:
可见不同数据格式压缩后的解压耗时时间差别不大,优劣排名PB>FB。
而反序列化耗时方面PB>FB,因此采用PB+GZIP优于FB+GZIP

结论

验证结果表明,选择PB更适合,传输前仍然需要GZIP压缩。

集成成本

分析了协议本身的优劣,我们再来看一下不同协议的使用成本。

1. android端几种方式的jar包大小对比

协议 jar大小(kb)
msgpack 114
pb-lite 241
fb 11

2. 集成难度对比

  • Msgpack
    优势:无需编译java文件
    缺点:解析需要自己写,没有封装类,序列化和反序列化需要严格对齐格式及顺序,容易出错;另外空字段需要占位;数据结构有嵌套时使用Extension封装较麻烦。还有其他的问题还未验证到。

  • Fb&pb
    优势:解析工作已经在编译生成的类中完成,只需要序列化工作;序列化时各字段顺序不必按指定顺序,也可不填写某些字段;两端只需维护一个schema文件即可。
    缺点
    需编译,先编写schema再编译成各平台语言。有一定的维护成本。
    数据和对象样式有强关联,用起来不是很灵活,比较适合数据协议固定且数据量大的通信场景。
    编译后的java类注释丢失,代码可读性较差
    测试复杂度提高
    fb的使用比pb更麻烦一点,多了一步数据索引的创建。

  • Json
    优点:数据透明,使用方便,测试成本低
    缺点:不安全,解析耗时,内存耗费高

几句话总结

选择哪种通信协议要根据自己的场景结合不同协议的特性综合考虑,没有一种绝对最优的协议,只有最合适的
除了协议本身的优劣之外,还需要综合考虑集成成本、对包大小影响等因素。
最后,最重要的,关键要看想要解决的问题是什么。如果从使用成本,方便性上选择,那json是最佳,那就要牺牲内存和时间。如果从解析速度上考虑,那就要降低对包大小等方面的要求。

推荐阅读更多精彩内容