① Protobuf语法介绍
我们先看看官方文档给出的定义和描述
简单来讲, ProtoBuf 是结构数据序列化方法,可简单类比于,其具有以下特点:
在protobuf中,协议是由一系列的消息组成的。因此最重要的就是定义通信时使用到的消息格式。一个Protobuf 消息(对应java类),由至少一个字段(对应Java类属性)组合而成。
消息的定义很简单,就是message关键字加上消息的名字
字段定义格式:
限定修饰符 | 数据类型 | 字段名称 | = | 字段编码
required:
表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
optional:
表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
repeated:
表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值。类比于Java这边的List
protobuf定义了一套基本的数据类型,几乎都映射了Java语言的基础数据类型(主要以Java为例)
详见下面表格
字段的命名方式与Java的命名方式大致一致
字段编码是一个序列化和反序列化的标记值,有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同。编码值的取值范围为 1~2^32(4294967296)
其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1-15),当然一般情况下相邻的2个值编码效率的是相同的,除非2个值恰好实在4字节,12字节,20字节等的临界区。比如15和16
1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用。protobuf 还建议把经常要传递的值把其字段编码设置为1-15之间的值
完整的消息定义示例
枚举的定义和Java 相同,使用 enum 关键字,但是有一些限制。
枚举值必须大于等于0的整数。
使用分号 ; 分隔枚举变量而不是Java 语言中的逗号 ,
示例:
你可以将其他消息类型用作字段类型。已我们现在IM的为例,在每一个CSSendLiveRoomMsgReq消息中包含ImMsgBody消息,此时可以在相同的.proto文件中定义一个ImMsgBody消息类型,然后在CSSendLiveRoomMsgReq消息中指定一个ImMsgBody类型的字段
示例:
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存,Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。
为了在.proto定义Oneof字段, 你需要在名字前面加上oneof关键字, 比如下面例子的ImMsgBody:
oneof的特性
② 了解一下ProtoBuf
我们在进行网络通信调用的时候,总是需要将内存的数据块经过序列化,转换成为一种可以通过网络流进行传输的格式。而这种格式在经过了传输之后再经过序列化,能还原成我们预想中的数据结构。
那么我们对于这种用于中间网络传输的数据格式就有一定的要求。首先它可以准确地描述数据内容,在此基础上我们则希望它尽量的小。
最开始流行起来的是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进行了概念的整理,并没有对每个细节都进行深入的梳理,可以当作概念科普来进行阅读。
③ protobuf3鍩虹璇娉
ProtoBuf 锛圙oogle Protocol Buffer锛夋槸鐢眊oogle鍏鍙哥敤浜庢暟鎹浜ゆ崲鐨勫簭鍒楃粨鏋勫寲鏁版嵁鏍煎紡锛屽叿鏈夎法骞冲彴銆佽法璇瑷銆佸彲鎵╁睍鐗规э紝鍚岀被鍨嬫湁甯哥敤鐨刋ML鍙奐SON锛屼絾鍏锋湁鏇村皬鐨勪紶杈撲綋绉銆佹洿楂樼殑缂栫爜銆佽В鐮佽兘鍔涳紝鐗瑰埆閫傚悎浜庢暟鎹瀛樺偍銆佺綉缁滄暟鎹浼犺緭绛夊瑰瓨鍌ㄤ綋绉銆佸疄鏃舵ц佹眰楂樼殑棰嗗煙锛岀洰鍓嶅凡缁忓彂灞曞埌protoc3 鐗堟湰銆
浼樼偣锛氱┖闂存晥鐜囬珮锛屾椂闂存晥鐜囪侀珮锛屽逛簬鏁版嵁澶у皬鏁忔劅锛屼紶杈撴晥鐜囬珮鐨
缂虹偣锛氭秷鎭缁撴瀯鍙璇绘т笉楂橈紝搴忓垪鍖栧悗鐨勫瓧鑺傚簭鍒椾负浜岃繘鍒跺簭鍒椾笉鑳界畝鍗曠殑鍒嗘瀽鏈夋晥鎬
澶囨敞锛氭渶鍚庣殑鏃堕棿绫诲瀷golang闇瑕佸紩鍏ュ寘 github.com/golang/protobuf/ptypes/timestamp ,瀹氫箟濡備笅
鐒跺悗 .protp 鏂囦欢闇瑕佸煎叆 google/protobuf/timestamp.proto
濡傛灉涓涓瀛楁佃 repeated 淇楗帮紝鍒欒〃绀哄畠鏄涓涓鍒楄〃绫诲瀷鐨勫瓧娈碉紝鐩稿綋浜 golang 閲岀殑鍒囩墖
濡傛灉浣犲笇鏈涘彲浠ラ勭暀涓浜涙暟瀛楁爣绛炬垨鑰呭瓧娈靛彲浠ヤ娇鐢╮eserved淇楗扮
绗涓涓鏋氫妇鍊肩殑鏁板煎繀椤绘槸0涓旇嚦灏戞湁涓涓鏋氫妇鍊硷紝涓涓鏁板煎彲浠ュ瑰簲澶氫釜鏋氫妇鍊硷紝蹇呴』鏍囨槑 option allow_alias = true; 涓嶆帹鑽愪娇鐢ㄨ礋鏁板
鍦ㄤ綘鐨 .proto 鏂囦欢涓鎸囧畾 service ,鐒跺悗鍦 service 閲屽畾涔 rpc鏂规硶 鍗冲彲锛岃佹敞鎰忔寚瀹氬弬鏁板拰杩斿洖鍊
gRPC 鍏佽镐綘瀹氫箟4绉嶇被鍨嬬殑 service 鏂规硶
瀹㈡埛绔浣跨敤瀛樻牴鍙戦佽锋眰鍒版湇鍔″櫒骞剁瓑寰呭搷搴旇繑鍥烇紝灏卞儚骞冲父鐨勫嚱鏁拌皟鐢ㄤ竴鏍
閫氳繃鍦 鍝嶅簲杩斿洖鍙傛暟 绫诲瀷鍓嶆彃鍏 stream 鍏抽敭瀛楋紝鍙浠ユ寚瀹氫竴涓鏈嶅姟鍣ㄧ鐨勬祦鏂规硶銆傚㈡埛绔鍙戦佽锋眰鍒版湇鍔″櫒锛屾嬁鍒颁竴涓娴佸幓璇诲彇杩斿洖鐨勬秷鎭搴忓垪銆 瀹㈡埛绔璇诲彇杩斿洖鐨勬祦锛岀洿鍒伴噷闈㈡病鏈変换浣曟秷鎭銆
閫氳繃鍦 璇锋眰鍙傛暟 绫诲瀷鍓嶆寚瀹 stream 鍏抽敭瀛楁潵鎸囧畾涓涓瀹㈡埛绔鐨勬祦鏂规硶銆傚㈡埛绔鍐欏叆涓涓娑堟伅搴忓垪骞跺皢鍏跺彂閫佸埌鏈嶅姟鍣锛屽悓鏍蜂篃鏄浣跨敤娴併備竴鏃﹀㈡埛绔瀹屾垚鍐欏叆娑堟伅锛屽畠绛夊緟鏈嶅姟鍣ㄥ畬鎴愯诲彇杩斿洖瀹冪殑鍝嶅簲銆
閫氳繃鍦ㄨ锋眰鍜屽搷搴斿墠鍔 stream 鍏抽敭瀛楀幓鍒跺畾鏂规硶鐨勭被鍨嬨備袱涓娴佺嫭绔嬫搷浣滐紝鍥犳ゅ㈡埛绔鍜屾湇鍔″櫒鍙浠ヤ互浠绘剰鍠滄㈢殑椤哄簭璇诲啓锛氭瘮濡傦紝 鏈嶅姟鍣ㄥ彲浠ュ湪鍐欏叆鍝嶅簲鍓嶇瓑寰呮帴鏀舵墍鏈夌殑瀹㈡埛绔娑堟伅锛屾垨鑰呭彲浠ヤ氦鏇跨殑璇诲彇鍜屽啓鍏ユ秷鎭锛屾垨鑰呭叾浠栬诲啓鐨勭粍鍚堛