首页 > 其他分享 >【Protobuf(四)】消息格式

【Protobuf(四)】消息格式

时间:2022-11-11 14:39:31浏览次数:44  
标签:map Protobuf 10 repeated tag 消息 MyProto 格式 type


protobuf是一种平台语言无关的消息序列化协议,相比于传统的json、xml,序列后的空间更小,但是无法自解释,需要结合额外的proto定义文件才能反序列化,当然这样也更安全。下面记录一下protobuf消息格式。

protobuf消息序列化后是多个key-value对,每一个字段对应一个key-value对。key-value遵循如下格式:

tag|type (length) data

tag|type:tag指的是字段的序号,这个一旦定义就不能更改,否则无法解析。type用于标识key-value对的类型,不同类型的解析方式及格式有区别。总共有这些type:

【Protobuf(四)】消息格式_嵌套

6种类型,需要3个bit。不过后来3和4废弃掉了,所以实际在用的只有0,1,2,5

0:表示这个字段是一个使用Varints编码的整数值;

1:64bit的定长数值;

2:string,嵌入式消息或者开启了packed模式的repeated字段;

4:32bit的定长数值;

这里着重看0和2两种type。tag|type的计算方式是(tab << 3) | type,最终的数值也按照Varints编码。

length:如果type是2,会有这个部分;

data:存放数据;

简单类型

看一个简单的例子:

message TestMsg1 {
int32 a = 1;
string b = 2;
}

@Test
public void test4() {
MyProto.TestMsg1 msg = MyProto.TestMsg1.newBuilder()
.setA(8)
.setB("123")
.build();
printHex(msg.toByteArray());
}

消息为:8, 8, 18, 3, 49, 50, 51

a字段(前两个字节):

(tag << 3 | type) = (0000001 << 3) | 0 = 0000 1000 = 8
data = 8

b字段:

(tag << 3 | type) = (0000010 << 3) | 2 = 0001 0010 = 18
length = 3
data = "123" = 49, 50, 51

 

嵌套消息

message TestMsg1 {
int32 a = 1;
string b = 2;
}

message TestMsg2 {
TestMsg1 msg = 1;
}

@Test
public void test4() {
MyProto.TestMsg1 msg = MyProto.TestMsg1.newBuilder()
.setA(8)
.setB("123")
.build();
printHex(msg.toByteArray());
}

10, 7, 8, 8, 18, 3, 49, 50, 51

前两个字节是外层消息的字段,也就是TestMsg1 msg,后面部分和上面的例子一样。

(tag << 3 | type) = (0000001 << 3) | 2 = 0000 1010 = 10
length = 7

所以,对于type=2的嵌套消息,length部分是消息所占用的总总字节数。

 

重复字段

对于整数类型的repeated字段,比如int32。如果开启了[packed=true]模式,那么会对重复数据格式做压缩。使用type=2的类型编码,重复元素的tag|type部分只出现一次,其后的length部分标识重复元素所占用的总字节数。在proto3中,packed模式默认开启开proto2中需要手动打开。如果没有开启,则重复元素会按照其本身的字段格式重复,tag|type部分会出现多次,空间利用率不太高,所以开启packed模式很有必要。

message TestMsg3 {
repeated int32 a = 1 [packed = false];
repeated int32 b = 2;
}

@Test
public void test5() {
MyProto.TestMsg3 msg = MyProto.TestMsg3.newBuilder()
.addA(1)
.addA(2)
.addA(3)
.addB(1)
.addB(2)
.addB(3)
.build();
printHex(msg.toByteArray());
}

字段a:8, 1, 8, 2, 8, 3。由于是int32,所以tag|type = 1 << 3 | 0 = 8,data部分都是1,重复了3次。总共需要6字节。

字段b:18, 3, 1, 2, 3。tag | type = 1 << 3 | 2 = 18,数据占用3字节,length=3,data是1,2,3。总共需要5字节。

对于其他类型的repeated字段,就是(tag << 3 | type) length data结构了,其中的data部分是多个key-value对。

为啥标量尅packed但是嵌套消息不行?

因为标量使用Varints编码,不需要length部分指定数据长度,从MSB位就可以知道读多少字节;但是嵌套消息不行,必须通过length指定长度,所以需要重复tag|type length部分。

 

map类型消息

其实,protobuff里并没有单独为map结构定义序列化协议,map结构与repeated一个对应的entry结构等价。来看例子:

message TestMsg4 {
map<string, int32> data = 1;
}

message TestEntry {
string key = 1;
int32 value = 2;
}

message TestMsg5 {
repeated TestEntry data = 1;
}

也就是这的TestMsg4 等价于TestMsg5 + TestEntry,写一个例子:

@Test
public void test7() {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

MyProto.TestMsg4 msg = MyProto.TestMsg4.newBuilder()
.putAllData(map)
.build();

printHex(msg.toByteArray());
System.out.println();

MyProto.TestEntry entry1 = MyProto.TestEntry.newBuilder()
.setKey("a")
.setValue(1)
.build();
MyProto.TestEntry entry2 = MyProto.TestEntry.newBuilder()
.setKey("b")
.setValue(2)
.build();
MyProto.TestEntry entry3 = MyProto.TestEntry.newBuilder()
.setKey("c")
.setValue(3)
.build();
MyProto.TestMsg5 msg1 = MyProto.TestMsg5.newBuilder()
.addData(entry1)
.addData(entry2)
.addData(entry3)
.build();
printHex(msg1.toByteArray());
}

输出:

10, 5, 10, 1, 97, 16, 1, 10, 5, 10, 1, 98, 16, 2, 10, 5, 10, 1, 99, 16, 3 
10, 5, 10, 1, 97, 16, 1, 10, 5, 10, 1, 98, 16, 2, 10, 5, 10, 1, 99, 16, 3

可以看到,序列化后的字节流完全一致。

以repeated的视角:

看一下“10, 5, 10, 1, 97, 16, 1, 10, 5, 10, 1, 98, 16, 2, 10, 5, 10, 1, 99, 16, 3”

TestEntry消息是一个嵌套消息,type是2。(tag << 3 | type) = (1 << 3 | 2) = 10,length是5。data部分就是string+int32,也就是10,1,97,16,1。其中的10,1,97表示string,16,1表示int32。这是一个repeated元素的字节流。

例子里重复了3次,所以后面的两个元素类似。所以,map确实是等价于repeated entry。

因为是repeated嵌套消息,所以每一个元素都需要将meta部分重复编码,也就是外侧消息和嵌套消息里的(tag<<3|type)这部分数据,都是重复的,有一定的空间浪费。

如何优化?

message TestMsg6 {
repeated string key = 1;
repeated int32 value = 2;
}

直接定义一个消息类型,里面有两个repeated的字段,分别是key和value。这样有两个好处:

一是repeated字段不再是嵌套消息,较少了嵌套消息meta定义的开销;

二是如果是标量类型,还可以利用packed模式进一步优化空间;

@Test
public void test8() {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

MyProto.TestMsg6 data = MyProto.TestMsg6.newBuilder()
.addKey("a")
.addKey("b")
.addKey("c")
.addValue(1)
.addValue(2)
.addValue(3)
.build();

printHex(data.toByteArray());
}

输出:10, 1, 97, 10, 1, 98, 10, 1, 99, 18, 3, 1, 2, 3。总共14个字节,比之前的21字节少了7字节!!!

当然这样定义也有弊端,在序列化与反序列化时需要额外处理map数据,转成两个list,使用起来不那么直观易懂。需要权衡。

所以如果在开发过程中如果更看重性能,可以使用优化后的map结构,反之使用原始的map即可。

 

标签:map,Protobuf,10,repeated,tag,消息,MyProto,格式,type
From: https://blog.51cto.com/u_15873544/5844829

相关文章

  • .net 分布式架构之业务消息队列
    .net业务消息队列是应用于业务的解耦和分离,应具备分布式,高可靠性,高性能,高实时性,高稳定性,高扩展性等特性。大量的业务消息堆积能力;无单点故障及故障监控......
  • ActiveMQ消息中间件的使用
    一、ActiveMQ的介绍。ActiveMQ是Apache出品,最流行的,能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE1.4规范的JMSProvider实现。1、主要特点:......
  • 【Protobuf(一)】proto文件的几个参数的含义
    1.package:定义message的包名。包名的含义与平台语言无关,这个package仅仅被用在proto文件中用于区分同名的message类型。可以理解为message全名的前缀,和message名合起来唯一......
  • 封装好的返回操作消息提醒工具类
    packagecom.ruoyi.common.core.domain;importcom.baomidou.mybatisplus.core.metadata.IPage;importcom.ruoyi.common.utils.StringUtils;importjava.util.Collec......
  • BFC(块级格式化上下文)
    BFC在MDN上面是这样定义的:块格式化上下文(BlockFormattingContext,BFC)是Web页面的可视CSS渲染的一部分,是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的......
  • simpread-(128 条消息) Three.js 模型隐藏或显示_郭隆邦技术博客的博客 - CSDN 博客_t
    Three.js模型隐藏或显示材质属性.visible查看Three.js文档的基类Material,可以知道材质属性.visible的作用就是控制绑定该材质的模型对象是否可见,默认值是true,LineBasi......
  • [JavaScript]格式化时间
    转载自网络 constformatDate=(time,fmt)=>{vardate=newDate(time);varformat=fmt||'YY-MM-DDhh:mm:ss'varyear=date.getFullYear(......
  • SwiftUI 与ObjC混编和消息传参
    SwiftUI同OC混编OC打开SwiftUI页面1、创建OC主工程2、添加Swift文件,此时会弹窗提醒自动创建一个桥接文件,点击确定,创建文件3、在SwiftUI文件中创建被oc调用的控制器和......
  • java中常见的JSON格式转换方法
    来源:https://blog.csdn.net/qq_42688149/article/details/1222755401JsonLib示例packagecom.jsonDemo;importnet.sf.json.JSONObject;importjava.util.HashMap;......
  • cesium 加载geoJson格式的图斑
    Cesium加载geoJson格式的图斑方法://首次进来判断是否存在图斑if(this.geoSource){this.Global.viewer.dataSources.remove(this.Global.viewer.dataSources.get......