Protobuf 使用和原理

1. protobuf 简介

命名:Protocol Buffers— 协议缓冲区

1.1. 发展背景

Protobuf 的诞生之初是为了解决服务端新旧协议(高低版本)兼容性问题,同时被寄予2 个特点:

  1. 可以很容易地引入新字段
  2. 数据格式可以用各种语言来处理(Java,C++ 等各种语言)

发展历程:

  1. 2001年在谷歌诞生
  2. 2008年2.0版本对外开源
  3. 2016年3.0版本发布
  4. 2022年更新至3.20

Protobuf 对外开源是从Protobuf2 开始

1.2. 优缺点

1.2.1. 优点

性能:

  • 体积小,序列化后,数据大小可缩小3-10倍
  • 序列化速度快,比XML和JSON快20-100倍
  • 传输速度快,因为体积小,传输起来带宽和速度会有优化

使用:

  • 使用简单,proto编译器自动进行序列化和反序列化
  • 维护成本低,多平台仅需维护一套对象协议文件(.proto)
  • 向后兼容性(扩展性)好,不必破坏旧数据格式就可以直接对数据结构进行更新
  • 加密性好,Http传输内容抓包只能看到字节

使用范围:跨平台、跨语言(支持Java, Python, Objective-C, C+, Dart, Go, Ruby, and C#等),可扩展性好

1.2.2. 缺点

  • 功能,不适合用于对基于文本的标记文档(如HTML)建模,因为文本不适合描述数据结构
  • 通用性较差:json、xml已成为多种行业标准的编写工具,而Protobuf只是Google公司内部的工具
  • 自解耦性差:以二进制数据流方式存储(不可读),需要通过.proto文件才能了解到数据结构

2. 使用

2.1. 消息类型

在 proto 中,所有结构化的数据都被称为 message。

syntax = "proto3";
package hello;

message helloworld 
{ 
   required int32     id = 1;
   required string    name = 2;
   optional int32     age = 3;
}

如果开头第一行不声明 syntax = “proto3”;,则默认使用 proto2 进行解析。
声明package,来防止命名冲突。 Packages是可选的。

2.1.1. 字段限制

  • required:消息体中必填字段,不设置会导致编解码异常;
  • optional:消息体中可选字段;
  • repeated:可重复字段(变长字段);

由于一些历史原因,repeated字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:

repeated int32 samples = 4 [packed=true];

2.1.2. 数据类型

完整数据类型映射—>《支持的全部数据类型》

.proto 类型C++类型Go 类型Java 类型
doubledoublefloat64double
int32int32int32int
int64int64int64long
sint32int32int32int
sint64int64int64long
boolboolboolboolean

2.1.3. 分配字段编号

每个消息定义中的每个字段都有唯一的编号
注意:

  • 范围 1 到 15 中的字段编号需要一个字节进行编码;
  • 范围 16 至 2047 中的字段编号需要两个字节;
  • 最小字段编号为1,最大字段编号为229-1 或 536,870,911;
  • 不能使用保留编号 19000 到 19999;

2.1.4. 保留字段

通过 reserved 确保删除的字段不会重复使用。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注意,不能在同一个 reserved 语句中混合字段名称和字段编号。如有需要需要像上面这个例子这样写。

通过完全删除某个字段或将其注释掉来更新消息类型,那么未来的用户可以在对该类型进行自己的更新时重新使用该字段号。如果稍后加载到了的旧版本 .proto 文件,则会导致服务器出现严重问题,例如数据混乱,隐私错误等等。

2.1.5. 默认字段规则

  • 字段名不能重复,必须唯一。
  • repeated 字段:可以在一个 message 中重复任何数字多次(包括 0 ),不过这些重复值的顺序被保留。

在 proto3 中,纯数字类型的 repeated 字段编码时候默认采用 packed 编码。

2.1.6. 枚举

在 message 中可以嵌入枚举类型。

message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}

枚举类型需要注意的是,一定要有 0 值。

  • 枚举为 0 的是作为零值,当不赋值的时候,就会是零值。
  • 为了和 proto2 兼容。在 proto2 中,零值必须是第一个值。

通过设置 allow_alias 为 true,允许将不同的枚举常量指定为相同的值。

2.2. Protobuf 工作流程

Protobuf 使用有2 个前置:

  • .proto 文件
  • protoc 编译器

《编译工具下载》

工作流程:

graph LR
.proto文件-->protoc编译器
protoc编译器-->C++/Python/Jave等平台目标文件
C++/Python/Jave等平台目标文件-->文件导入项目
文件导入项目-->引入Google提供的相应库
引入Google提供的相应库-->开始序列化/反序列化

2.2.1 编译proto文件

执行protoc命令对.proto文件进行编译。Linux系统通过 help protoc 查看protoc命令的使用详解。

protoc --proto_path=$SRC_DIR --cpp_out=$DST_DIR  xxx.proto 
  • –proto_path= S R C D I R 表示从 SRC_DIR 表示从 SRCDIR表示从SRC_DIR目录下读取proto文件。
  • –cpp_out=$DST_DIR 表示生成的C++代码保存路径
  • xxx.proto:要针对哪个proto文件生成接口,例如 hello.proto

–proto_path 有一个别名 -I 。

2.3. 使用建议

  1. 字段标识号,尽量控制在1-15;
  2. 若使用字段出现负数,考虑 sint32/sint64 类型;
  3. 若使用字段出现比较大的正数,考虑使用 fixed32/fixed64 类型;
  4. 对于 repeated 字段,尽量增加 packed=true 修饰;

3. 原理

《google官方原理介绍》

3.1. 编码格式

protobuf采用TLV(tag-length-value)编码格式。

  • tag:字段的唯一标识;
  • length:表示value数据的长度,length不是必须的,固定长度的value,没有length;
  • value:数据本身的内容
    在这里插入图片描述

3.1.1. 字段唯一标识——tag

tag值: 由field_number和wire_type两部分组成。
在这里插入图片描述

  • field_number: message 定义字段时指定的字段编号;
  • wire_type: 根据这个类型选择不同的 Value 编码方案;
wrie_type编码方案编码长度存储方式对应的数据类型
0Varint
(负数为ZigZag)
变长(1-10个字节)T-Vint32,int64,uint32,uint64,bool
enum,int32,int64(负数使用)
164-bit固定8个字节T-Vfixed64,sfixed64,double
2Lenght-deliml变长T-L-Vstring,bytes,repeated
532-bit固定4个字节T-Vfixed32,sfixed32,float

字段标识号(Field_Number),尽量控制在1-15。超过了则需要2个字节或更多。

3.1.2. 补充 packed 编码

在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。

  • [packed = false] 时的结构:Tag-Length-Value-Tag-Length-Value-Tag-Length-Value…
  • [packed = true] 时的结构:Tag-Length-Value-Value-Value…

3.2. 编码算法

Protobuf 中编码有两种:Varints 和 ZigZag。

ZigZag用于解决varint对负数编码效率低的问题。负数推荐使用 sint32 或 sint64。

3.2.1. 补码概念回顾

  • 原码:最高位为符号位,剩余位表示绝对值;
  • 反码:除符号位外,对原码剩余位依次取反;
  • 补码:对于正数,补码为其自身;对于负数,除符号位外对原码剩余位依次取反然后+1。

3.2.2. Varints

Varint 是一种使用一个或多个字节序列化整数的方法,也可以说是一种压缩算法,值越小的数字使用越少的字节数。压缩的依据是:越小的数字,越经常使用。

3.2.2.1. Varints 编码

Varints 的编码规则如下【注:大端字节序下】:

  1. 将数值转换为二进制,从最低位开始,自右至左每 7 位作为一组进行分割
  2. 翻转组。
  3. 在每一组最前面插入一位最高有效位(msb),凑成一个字节(8 位)。最后一组插入 0,表示后面没有字节出现;其他组插入 1 ,表示后面还有字节出现。
  4. 此时每一组都有 8 位,即一组就是一个字节,将结果转换为十六进制输出。

以 150 为例,首先转换为二进制:

1001 0110

7 位一组进行分割:

000 0001, 001 0110

翻转组:

001 0110, 000 0001

每一组最前面插入 msb,除最后一组插入 0 外,其余组插入 1:

1001 0110, 0000 0001

转换为十六进制表示:

96, 01
3.2.2.2. Varints 解码

Varints 的解码就是对编码的逆操作,以 150 的编码结果进行解码为例:

  1. 将编码后数据(十六进制)转换为二进制
  2. 去除每个字节最高位的 msb
  3. 翻转,然后转换为 10 进制输出

以 96, 01 为例,首先转换为二进制:

1001 0110, 0000 0001

去除每个字节最高位的 msb:

001 0110, 000 0001

翻转:

000 0001, 001 0110

转换回十进制:

128 + 16 +4 + 2 = 150

3.2.3. ZigZag

3.2.3.1. ZigZag 编码

Zigzag 编码规则

  • 有符号整数映射成无符号整数,再使用 varint 编码
    Zigzag 映射函数

Zigzag 映射函数

  • h(n) = (n << 1) ^ (n >> 31), n为sint32时
  • h(n) = (n << 1) ^ (n >> 63), n为sint64时

整数的补码(十六进制)与hash函数的对应关系如下:

nhexh(n)ZigZag (hex)
000 00 00 0000 00 00 0000
-1ff ff ff ff00 00 00 0101
100 00 00 0100 00 00 0202
-2ff ff ff fe00 00 00 0303
200 00 00 0200 00 00 0404
-64ff ff ff c000 00 00 7f7f
6400 00 00 4000 00 00 8080 01

下面以int32类型的数-2为例,分析它的编码过程。如下图所示:
在这里插入图片描述

3.2.3.2. ZigZag 解码

解码:

  • h(n) = (n >>> 1) ^ -(n & 1)
3.2.3.3. C++ 实现
#include <iostream>
using namespace std;

// zigzag 编码
unsigned int zigzag_encode_32(int val)
{
    return (unsigned int)((val<<1)^(val>>31));
}

// zigzag解码
int zigzag_decode_32(unsigned int val)
{
    return (int)((val>>1) ^ -(val&1));
}
 
int main()
{
    int n;
    while(1)
    {
        cout <<"\n请输入原码:";
        cin >> n;
        unsigned int zn = zigzag_encode_32(n);
        int uzn = zigzag_decode_32(zn);
        cout << "ZigZag编码:" << zn << ", 解码:" << uzn << endl;
    }
    
    return 0;
}

执行:

请输入原码:0
ZigZag编码:0, 解码:0

请输入原码:-1
ZigZag编码:1, 解码:-1

请输入原码:1
ZigZag编码:2, 解码:1

请输入原码:-2
ZigZag编码:3, 解码:-2

请输入原码:2
ZigZag编码:4, 解码:2

请输入原码:-3
ZigZag编码:5, 解码:-3

请输入原码:3
ZigZag编码:6, 解码:3

请输入原码:-64
ZigZag编码:127, 解码:-64

请输入原码:64
ZigZag编码:128, 解码:64

请输入原码:-65
ZigZag编码:129, 解码:-65

请输入原码:65
ZigZag编码:130, 解码:65

3.3. 总结

  • Varint编码:有效降低了数据量,但对大的正数和负数并不友好;
  • Zigzag编码:解决了负数编码过长问题;
  • Protobuf围绕着T-L-V 存储方式,不同的数据类型采用不同的编码方式。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值