protobufjs( https://github.com/protobufjs/protobuf.js )可以让我们再浏览器中使用protobuf,下面的例子是如何在普通页面中,直接使用protobufjs。
全局安装protobufjs,使用pbjs直接生成proto文件对应的js protobuf描述文件,这样就可以在普通页面中直接使用了。
在网络通信中,我们经常需要通过一种约定俗称的方式来进行数据交互。在这中间,有很多如xml,json等等很常见的数据格式。
xml是一种标签语言,通过<tag>content</tag>来描述数据内容,出现时间最早,应用最广泛,但是传输效率低。
json作为一种轻量级的数据交换格式,其无效的字符数远远低于xml,且原生被js支持,故经常出现在前端请求中以及app与服务器的数据交互。
protobuf是google的一种数据交换格式,相比于其他的几种交互方式,其优势在于数据的重编码,在传输时会将其编译为二进制数据流,而当需要使用时再进行反序列化。
protobuf支持几乎绝大多数的语言。
通过proto文件,我们可以了解到一个数据的详细类型。
在官方示例中可以看到一个数据类型的声明方法:message 对象 {}。在{}中则是添加相应的字段描述,每个字段描述如下
数据类型为protobuf声明的数据类型,每个类型在不同的语言上均有特定的映射。
或可以是一个声明的自定义数据类型。
声明完成后通过protoc编译为特定语言的头文件。
引入头文件后 首先声明一个对象
通过person.xxx可以直接对某个字段进行赋值
在序列化过程中有可能因为required未被赋值而导致序列化失败
序列化成功后便可以将二进制数据发送出去
收到对应的数据后,我们需要从二进制数据中获取对应的对象
person = addressbook_pb2.Person()
person.ParseFromString(pb_bin)
我们在进行网络通信调用的时候,总是需要将内存的数据块经过序列化,转换成为一种可以通过网络流进行传输的格式。而这种格式在经过了传输之后再经过序列化,能还原成我们预想中的数据结构。
那么我们对于这种用于中间网络传输的数据格式就有一定的要求。首先它可以准确地描述数据内容,在此基础上我们则希望它尽量的小。
最开始流行起来的是XML,可扩展标记语言。由于它可以用来标记数据、定义数据类型,所以用户可以自己定义数据自己的语言,从而让对不同的数据结构化成统一的格式称为了可能。
而另外一个我们熟知的则是JSON(JavaScript Object Notation, JS 对象简谱)。尽管JSON中缺少了XML中的标签属性等描述方式,但是足够简介和清晰的层次结构使得其成为了必XML更受欢迎的数据交换格式。
同一份数据显然JSON的数据量比XML所使用的空间更少。那么空间省略在哪里呢?一方面是json使用更简单的字符来定义数据间的关联关系;另一方面是JSON减少了对数据类型的描述。但是丢少的数据类型再哪里呢?
以Java中的 OpenFeign 举例,JSON中缺少的类型定义被定义道程序中的接口中了。当进行序列化与反序列化时,JSON格式并不记录数据的类型,具体的数据类型在序列化方与反序列化方通过事先约定的接口来进行定义。这样就减少了信息传输过程中的信息量,从而让数据得以压缩。
但是JSON由于没有定义数据类型,所以在传输的过程中实际上就都是文本流,那么这种方法还可以进一步压缩吗?
结合上文的讨论,我们先说结论:方法是有的,并写当前的实现方式是ProtoBuf。但在此之前我们先来了解一下ProtoBuf。
我们可以先看看官方给出的定义与描述:
同样的,ProtoBuf也是一种支持序列化反序列化的方法,并且他具有很多优点:
实际上,ProtoBuf提供了一种通用的数据描述方式,这种定义数据的方式是通用的,就如同JSON或者XML一样。
接下来我们来来回答本节一开始的问题,针对JSON来说,ProtoBuf是如何将体积变得更小的呢?答案很简单,就是为数据序列化反序列化提供更多的先验知识。
本文暂不过度深入ProtoBuf原理,但是可以通过一张图来进行简要说明():
ProtoBuf中的数据是按顺序进行排列,而整体的结构为若干个field,每一个field中由 Tag-[Length]-Value 组成。Length是可选的,而是否存在Length是通过Tag的类型来决定的。也就是说如果是指定的类型,比如int64,那我们就可以知道Value的长度,也就不用在依靠Length来对其空间进行描述(redis中的压缩列表也是这个思想)。
那么field应该对应的是什么字段呢?这个则是在序列化与反序列化时在ProtoBuf的服务端与客户端之间进行预先定义的。而因为提前定义了field的类型、排序,所以field本身可以不用对字段名、字段位置进行描述,只需要根据字段类型选用合适的二进制序列化方法,将字段本身的value值进行序列化传输即可。
稍微总结一下:
ProtoBuf通过对传输字段的名称、顺序进行预定义,从而在传输结构中只需要顺序的记录每个字段的类型标签和二进制值。
尽管上文和官方中都是以XML或者JSON来对ProtoBuf进行对比。但是因为ProtoBuf本身就是二进制序列化方式,所以从压缩比上比较感觉有点欺负人。
对应的在Java中二进制常用的序列化器有Kryo和Hessian。但事实上,由于Kryo和Hessian中都需要对Java类名和字段信息进行存储。而ProtoBuf则只有Tag-Length-Value的数据对,且Value更是有针对性的特殊编码,所以空间占用小的很多。
Kryo是专门针对Java进行优化了的。所以在使用的便捷性上来说Kryo则更加方便。但ProtoBuf是跨平台的,且由于进行了字段的顺序定义,所以似的ProtoBuf定义后的接口是可以向前兼容的(只向后追加字段),而这种优势是Kryo所没有的。
ProtoBuf是跨语言的,使用ProtoBuf的第一步是先定一个 proto 文件 ,而由于ProtoBuf 2和3语言版本的不同,其定义格式会有所不同,具体的细节还是得参考官方文档:https://developers.google.cn/protocol-buffers/docs/proto3
对于ProtoBuf 3 的定义文档我们可以按如下方法定义:
其中message关键字是定义的文件名,而 string、int32则是预定的字段类型,repeated则是描述字段为可重复任意多次的字段。
ProtoBuf通过这种形式的文件定义了传输信息的文件结构。
但是之前小节中我们知道了ProtoBuf是通过 Tag-[Length]-Value 组成的数据组来进行信息传输的,那么proto文件中定义的内容如何转换为实际传输的对象呢?
ProtoBuf的做法是,为每一种语言提供一个生成器protoc。通过使用protoc则可以根据.proto文件生成为一组java文件。对应的官方语法演示样例为:
官方的生成参考为:https://developers.google.com/protocol-buffers/docs/reference/java-generated
生成后的java文件将提供对应的实体以及数据的构造方法等文件,从而支持后续的使用。
需要注意的是,ProtoBuf是本质上是序列化方法,具体是通过Spring Cloud 的OpenFeign进行接口调用,还是通过grpc进行接口调用,都是可以的。
本文对ProtoBuff进行了概念的整理,并没有对每个细节都进行深入的梳理,可以当作概念科普来进行阅读。