文章目录

  • 1. protobuf 简介
    • 1.1. 发展背景
    • 1.2. 优缺点
      • 1.2.1. 优点
      • 1.2.2. 缺点
  • 2. 使用
    • 2.1. 消息类型
      • 2.1.1. 字段限制
      • 2.1.2. 数据类型
      • 2.1.3. 分配字段编号
      • 2.1.4. 保留字段
      • 2.1.5. 默认字段规则
      • 2.1.6. 枚举
    • 2.2. Protobuf 工作流程
      • 2.2.1 编译proto文件
    • 2.3. 使用建议
  • 3. 原理
    • 3.1. 编码格式
      • 3.1.1. 字段唯一标识——tag
      • 3.1.2. 补充 packed 编码
    • 3.2. 编码算法
      • 3.2.1. 补码概念回顾
      • 3.2.2. Varints
        • 3.2.2.1. Varints 编码
        • 3.2.2.2. Varints 解码
      • 3.2.3. ZigZag
        • 3.2.3.1. ZigZag 编码
        • 3.2.3.2. ZigZag 解码
        • 3.2.3.3. C++ 实现
    • 3.3. 总结

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 类型
double double float64 double
int32 int32 int32 int
int64 int64 int64 long
sint32 int32 int32 int
sint64 int64 int64 long
bool bool bool boolean

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 表示从 SRCD​IR表示从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 编码方案 编码长度 存储方式 对应的数据类型
0 Varint
(负数为ZigZag)
变长(1-10个字节) T-V int32,int64,uint32,uint64,bool
enum,int32,int64(负数使用)
1 64-bit 固定8个字节 T-V fixed64,sfixed64,double
2 Lenght-deliml 变长 T-L-V string,bytes,repeated
5 32-bit 固定4个字节 T-V fixed32,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函数的对应关系如下:

n hex h(n) ZigZag (hex)
0 00 00 00 00 00 00 00 00 00
-1 ff ff ff ff 00 00 00 01 01
1 00 00 00 01 00 00 00 02 02
-2 ff ff ff fe 00 00 00 03 03
2 00 00 00 02 00 00 00 04 04
-64 ff ff ff c0 00 00 00 7f 7f
64 00 00 00 40 00 00 00 80 80 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 存储方式,不同的数据类型采用不同的编码方式。

Protobuf 使用和原理相关推荐

  1. IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)

    本文由金蝶随手记技术团队丁同舟分享. 1.引言 跟移动端IM中追求数据传输效率.网络流量消耗等需求一样,随手记客户端与服务端交互的过程中,对部分数据的传输大小和效率也有较高的要求,普通的数据格式如 J ...

  2. Protobuf序列化的原理

    我们可以把序列化以后的数据打印出来看看结果 public static void main(String[] args) { UserProtos.User user = UserProtos.Use ...

  3. Android Protobuf应用及原理

    前言 之前一直忙于移动端日志SDK Trojan的开源工作,已十分稳定地运行在饿了么团队App中,集成了日志加密和解密功能.哎呀,允许我卖个狗皮膏药,不用不知道,用了就知道,从此爱不释手,Trojan ...

  4. 【转】Android Protobuf应用及原理

    前言 之前一直忙于移动端日志SDK Trojan的开源工作,已十分稳定地运行在饿了么团队App中,集成了日志加密和解密功能.哎呀,允许我卖个狗皮膏药,不用不知道,用了就知道,从此爱不释手,Trojan ...

  5. Protobuf序列化的原理-负数的存储

    在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所以如果采用varint编码表示一个负数,那么一定需要5个比特位.所以在protobuf中通过sint32/sint64类 ...

  6. Protobuf序列化的原理-存储格式

    protobuf采用T-L-V作为存储方式 tag的计算方式是 field_number(当前字段的编号) << 3 | wire_type 比如Mic的字段编号是1 ,类型wire_ty ...

  7. Protobuf序列化的原理-字符如何转化为编码

    "Mic"这个字符,需要根据ASCII对照表转化为数字. M =77.i=105.c=99 所以结果为 77 105 99 大家肯定有个疑问,这里的结果为什么直接就是ASCII编码 ...

  8. Protobuf序列化的原理-protobuf的基本应用

    使用protobuf开发的一般步骤是 1. 配置开发环境,安装protocol compiler代码编译器 2. 编写.proto文件,定义序列化对象的数据结构 3. 基于编写的.proto文件,使用 ...

  9. Protobuf序列化的原理-总结

    Protocol Buffer的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下: 序列化速度快的原因: a. 编码 / 解码 方式简单(只需要简单的 ...

最新文章

  1. SalttSack自动化运维(四)——JINJA模块
  2. 【BZOJ】1299: [LLH邀请赛]巧克力棒
  3. 知识星球!!!!!!
  4. 【Python学习系列四】Python程序通过hadoop-streaming提交到Hadoop集群执行MapReduce
  5. 首次合作带给我的感想
  6. C++中有关queue常用函数的用法及其注意要项
  7. 12123两小时没付款怎么办_机械厂上班的男朋友,一天十小时,周末不休,没时间陪我怎么办?...
  8. 2018怎么打开2019_2019 年,我还是没有摆脱 Micro USB
  9. 安装版mysql5.7_mysql5.7 安装版安装
  10. catia怎么将特征参数化_CATIA参数化建模及关系式的创建和使用 | 坐倚北风
  11. mysql乱码utfmb4_MySQL乱码问题以及utf8mb4字符集
  12. bilibili 解析_广西财经大学《殙》舞蹈解析
  13. 简单易懂源码解析字符串拼接
  14. 对于xfire动态调用webservice接口
  15. Java基础入门笔记
  16. indesign排版标点挤压_孔雀计划序——中文排版思路的重建
  17. AXD 调试经验,使用及问题
  18. 邓俊辉 《数据结构》笔记1 绪论
  19. 【bzoj3926】[Zjoi20150]诸神眷顾的幻想乡 后缀自动机+trie
  20. C语言循环队列的基本操作(init,enquene,dequene)与杨辉三角(C和C++<queue>)

热门文章

  1. Python学习之:如何根据经纬度来实现地图的可视化(将这些点在地图上标注出来)
  2. (附源码)SSM仓库管理系统 毕业设计 061015
  3. java区分手机号归属地_JAVA手机号码归属地查询
  4. sed命令实现匹配行下一行的替换
  5. ALL in Boom 日志记录 (ing ...
  6. 18年_PMO_实践
  7. 每天有2800万人在看片
  8. ArcGIS基础实验操作100例--实验3旋转矢量要素
  9. VMware:虚拟机磁盘空间不足怎么办
  10. js逆向案例-猿人学比赛题(中等及以下难度的)