Protobuf原理分析

详细分析protobuf(以下简称pb)数据序列化Tag-WireType-Value方式,对VARINT、带符号整型的详细分析,分别对int32, int64, uint32, uint64, sint32, sint64, bool, enum,fixed64, sfixed64, double, string, bytes, embedded messages, packed repeated fields, fixed32, sfixed32, float所有protobuf支持的数据类型进行说明。通过demo和验证过程,相信能帮忙到大家理解protobuf的原理。

Tag-WireType-Value

pb里描述每一个数据对象均按这一规则。

  1. Tag是每个字段后的1、2、3 ……
  2. WireType是指数据类型和Value的长度,下表列出全部类型:
TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float
  1. Value是指字段实际值

下面的例子均按照proto3的协议定义来编写,相比proto2,optional是默认的。详细请看官方文档

syntax = "proto3";
package test;
option java_package = "com.example.test";
option java_outer_classname = "TestProtos";

message User {
    int32 id = 1;
    string name = 2;
    repeated string icon_url = 3;
}
复制代码

如果数据设置如下:

{
  id : 10,
  name : "Jo"
}
复制代码

则编码后的数据为080a12024a6f

hex说明
08这是Tag和WireType部分,规则为 $tag << 3 | WireType。 tag:id(1),字段类型为VARINT(0),所以值为 1 << 3 | 0。
0a10的值。
12tag:id(2),字段类型为Length-delimited(2),所以值为 2 << 3 | 2。
02由于是字符串类型,这个段表示字符串的长度,"Jo"长度为2。
4aJ的ASCII码
6fo的ASCII码

可变长整形VARINT

一个int32来说,无论是1,还是65535,都会占用4个字节,而pb为了使数据更紧凑更节省空间,使用了可 变长的int来表示数字。小的数用1个字节来表示,大的数用多个字节来表示。VARINT就是一种可使用1个或多个字节来表示整型的方法。 每个VARINT的最后一个字节是一个标志位(msb),表示这个数字除了当前字节是否有后一个字节来一起表示。 剩下的低7位表示数字的实际数值。 例如数字1,用VARINT表示:

0000 0001
复制代码

由于不需要后面的字节来表示,所以msb为0,000 0001表示1。 数字300,这个比较复杂一点,编码后为08ac02,转为二进制:

1010 1100 0000 0010
复制代码

分析:

  1010 1100 0000 0010
→ 010 1100  000 0010 把msb去掉后
→ 000 0010  010 1100 pb使用的整形型是低位方式(little-endian),所以要把2个字节互换位置
→ 000 001 0010 1100  2个字节拼接在一起
→ 1 0010 1100        去掉前面的0
→ 256 + 32 + 8 + 4 = 300  2进制转换为10进制
复制代码

可见,编码如1、300这样值不高的数字时,结果为1个字节到2个字节之间,对于我们大多数的实际场景来说是非常适合的,能有效的节省编码长度。

有符号整型

前面章节中看到,所有VARINT(0)为0的都是用VARINT来表示的,包括的具体类型有int32, int64, uint32, uint64, sint32, sint64, bool, enum。但是sint32/sint64,与int32/int64在用于表示 负数 是有区别的。如果使用int32表示一个负数,结果会占用10个字节,实际上用了一个很大无符号数来表示。如果使用sint32,会使用ZigZag编码方式来提高效率。

ZigZag

ZigZag编码是用一种正负数的交错方式来表达,当数值的绝对值小的时候,会比使用int32的方式节省很多字节。例如-1编码为1,1编码为2,-2编码为3,如下表:

带符号数编码后
00
-11
12
-23
21474836474294967294
-21474836484294967295

例如:

message Signed {
    int32 a = 1;
    sint32 b = 2;
}
复制代码

如果数据设置如下:

{
  a : -10,
  b : -10
}
复制代码

则编码后的数据为08f6ffffffffffffffff011013

hex说明
08tag:id(1),int32字段类型为VARINT(0),所以值为 1 << 3 | 0。
f6ffffffffffffffff01用一个很大的正数来表示的-10
10tag:id(2),sint32字段类型为VARINT(0),所以值为 2 << 3 | 0。
13用ZigZag表示的-10, 0x13 = 19,符合上面编码表推算出来的值

sint32的值用这个公式表达:(n << 1) ^ (n >> 31)

可见sint32只用1个字节表示-10,int32要用10个字节。

int32分析

接下来分析上面那串10个字节的int32:

内容比较长,建议copy出来看

f6ffffffffffffffff01
→ 1111 0110 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0000 0001 转成2进制
→  111 0110  111 1111  111 1111  111 1111  111 1111  111 1111  111 1111  111 1111  111 1111  000 0001 把msb去掉后
→ 000 000 1111 1111  1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0110 把字节顺序反转,再拼在一起
→            ff         ff        ff        ff        ff        ff        ff        f6 去掉前面的0,转为16进制,这个数就是0xffffffffffffffff - 9 = -10(可以打印看Integer.toHexString(-10)的16进制字符串)。
复制代码

非VARINT的数字

如上图的字段类型表,float/fixed32/sfixed32的字段类型为5,double/fixed64/sfixed64的字段类型为1,它们使用固定长度方式来记录数字,同样也是使用little-endian字节顺序,有符号类型也是使用ZigZag方式。

enum枚举

enum枚举也是用VARINT来表示的,enum枚举是带默认值的,默认为0的元素,默认值是可以不编码的,因为解码端默认就是0。空代表元素0,01代表元素1,02代表元素2。

bool布尔值

bool布尔值也是用VARINT来表示的,bool是带默认值的,默认为false,默认值是不编码的,因为解码端默认就是false。空代表false,01代表true。

string字符串

string字段类型为Length-delimited(2),最早的例子里出现过,tag << 3 | 2,后面跟一个VARINT来表示字符串长度,紧跟着就是字符串的内容。

bytes二进制数据

编码方式跟string一致,但不会对字符串编码进行处理。转换成Java类型为byte[]。

嵌套对象

一个类型嵌套另外一个类型:

message Test1 {
    int32 a = 1;
}

message NestTest {
    Test1 t = 1;
}
复制代码

设置a的值为300,编码后输出为:0a0308ac02。上面的例子已经分析过300编码后为08ac02,对比可以看出,嵌套多出了前面0a03。嵌套对象的字段类型为Length-delimited(2),因此 tag << 3 | 2 = 0a,后面跟着嵌套对象的长度,即08ac02的长度为3,因此为03。

Repeated标记可重复

在proto2里,Repeated标记的字段可以是0个或多个的数值,在Java里翻译过来是array。编码后Repeated的数值可以是连续,也可以不连续,因为每次编码都是按Tag-WireType-Value,最后把各个值merge合并。 举例:

message RepeatedTest {
    repeated int32 a = 1;
}
复制代码

数据为:1, 2, 3 编码后:080108020803

0801  tag << 3 | 0  , 1
0802  tag << 3 | 0  , 2
0803  tag << 3 | 0  , 3
复制代码

同一个tag出现多次,解码的时候把多次进行merge处理,还原出array结果。

Packed Repeated标记为打包一块可重复

表示把数据打包在一起形成一个类似于嵌套的形式,打包一起的数据不能分开,必须连续。相比repeated少编码了重复的tag。 举例:

message RepeatedPackedTest {
    repeated int32 a = 1 [packed=true];
}
复制代码

数据为:1, 2, 3 编码后:0a03010203

0a  tag << 3 | 2 (Length-delimited(2))
03  payload,后面的数据长度
01  1
02  2
03  3
复制代码

总结&建议

  • proto2版本所有字段加上optional,proto3默认为optional,不用手动再添加。
  • 有负数的数值时使用sint32/sint64
  • 正数没超过2 ^ 28 - 1 = 268435455,则使用int32类型,否则使用fixed32类型,当然超过了2 ^ 32 - 1,就要使用int64/fixed64了。

源码

至此把protobuf所有数据类型的原理进行了分析,上面的示例代码可到此下载:github.com/itvincent-g… 。测试代码在app/src/test/../ExampleUnitTest.java

转载于:https://juejin.im/post/5a55b4b66fb9a01c96580717

### Go语言中的Protobuf工作原理 #### 协议缓冲区简介 协议缓冲区(Protocol Buffers),简称 Protobuf,是一种轻便高效的结构化数据存储格式,可对结构化数据进行序列化。这种技术可用于通信协议、数据存储等众多领域。 #### .proto 文件定义 为了使用 Protobuf 序列化方式,在项目中首先要编写 `.proto` 文件[^4]。此文件用于定义消息类型以及其字段。例如: ```protobuf syntax = "proto3"; package example; message Person { string name = 1; int32 id = 2; string email = 3; } ``` 上述代码片段展示了如何创建一个新的 `Person` 消息类型及其三个字段:名字(`name`)、ID (`id`) 和电子邮件地址(`email`)。 #### 自动生成的序列化/反序列化逻辑 一旦定义好了`.proto` 文件之后,就可以利用官方提供的工具来自动生成目标语言对应的源码[^3]。对于Go来说,这通常意味着生成一组能够方便地操作这些消息类型的函数库。通过这种方式,开发者不再需要担心底层的数据表示形式;相反,可以直接专注于应用程序本身的开发上。 #### 数据交换过程 当涉及到跨平台间传输信息时——比如客户端向服务器发送请求或接收响应——实际传送的是二进制流而非原始的消息对象本身。因此,在发出之前需调用特定的方法将原生的对象转换成字节数组;而在另一端接收到该数组后,则应将其还原回原来的形式以便进一步处理。整个过程中涉及到了两个重要概念:“编码”(Serialization) 和 “解码” (Deserialization)[^2]。 #### 实际案例分析 假设有一个简单的聊天程序,其中包含了用户登录功能。此时可以在相应的 `.proto` 文件里声明如下所示的一个新消息类型: ```protobuf // 来源于引用 [4] message IMLoginReq{ string user_name=1; string password=2; UserStatType online_status=3; ClientType client_type=4; string client_version=5; } ``` 接着借助 protoc-gen-go 插件为这段描述生成配套的支持包。最终得到的结果将是类似于下面这样的接口实现: ```go import ( "example" ) func main() { loginRequest := &example.IMLoginReq{ UserName: "test_user", Password: "password123", OnlineStatus: example.UserStatType_ONLINE, ClientType: example.ClientType_WEB, ClientVersion:"v1.0.0", } data, err := proto.Marshal(loginRequest) if err != nil { log.Fatal("marshaling error:", err) } newLoginRequest := &example.IMLoginReq{} err = proto.Unmarshal(data, newLoginRequest) if err != nil { log.Fatal("unmarshaling error:", err) } fmt.Printf("%v", newLoginRequest) } ``` 以上就是关于Go语言环境下运用 Protocol Buffer 技术的一些基本介绍和具体实例说明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值