【RPC】—Protobuf编码原理

news2025/1/10 17:10:03

Protobuf编码原理

⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记链接👉https://github.com/A-BigTree/Code_Learning
⭐⭐⭐⭐⭐⭐


Spring专栏👉https://blog.csdn.net/weixin_53580595/category_12279588.html

SpringMVC专栏👉https://blog.csdn.net/weixin_53580595/category_12281721.html

Mybatis专栏👉https://blog.csdn.net/weixin_53580595/category_12279566.html

如果可以,麻烦各位看官顺手点个star~😊

如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆


文章目录

  • Protobuf编码原理
    • 1 前言
    • 2 Base 64
      • 2.1 技术背景
      • 2.2 工作原理
    • 3 Base 128
    • 4 Base 128 Varints
      • 4.1 基本概念
      • 4.2 例子
      • 4.3 对整数进行编码
    • 5 Protobuf编码
      • 5.1 有符号整型
      • 5.2 定长数据
      • 5.3 字符串
      • 5.4 字段类型和字段名称
      • 5.5 嵌套消息
      • 5.6 重复消息编码规则
      • 5.7 字段顺序
        • 编码结果与字段顺序无关
        • 相等消息编码后结果可能不同


1 前言

Protobuf的编码是基于变种的Base128的,在学习Protobuf编码或者是Base128之前,先来了解下Base64编码。

2 Base 64

2.1 技术背景

当我们在计算机之间传输数据时,数据本质上是一串字节流。TCP 协议可以保证被发送的字节流正确地达到目的地(至少在出错时有一定的纠错机制),所以本文不讨论因网络因素造成的数据损坏。

但数据到达目标机器之后,由于不同机器采用的字符集不同等原因,我们并不能保证目标机器能够正确地“理解”字节流。Base 64 最初被设计用于在邮件中嵌入文件(作为 MIME 的一部分):它可以将任何形式的字节流编码为“安全”的字节流。

**何为“安全“的字节?**先来看看 Base 64 是如何工作的。

2.2 工作原理

假设这里有四个字节,代表要传输的数据:

10100010  00001001  11000010  11010011

首先将这字节流按每 6 个 bit 为一组进行分组,剩下少于 6 bits 的低位补 0:

101000  100000  100111  000010  110100  110000

然后在每一组 6 bits 的高位补两个 0:

00101000  00100000  00100111  00000010  00110100  00110000

Base 64编码对照表如下图:

在这里插入图片描述

对照Base 64的编码对照表,字节流可以用ognC0w来表示。

另外: Base64 编码是按照 6 bits 为一组进行编码,每 3 个字节的原始数据要用 4 个字节来储存,编码后的长度要为 4 的整数倍,不足 4 字节的部分要使用 pad 补齐,所以最终的编码结果为ognC0w==

任意的字节流均可以使用 Base 64 进行编码,编码之后所有字节均可以用数字字母+ / = 号进行表示,这些都是可以被正常显示的 ascii 字符,即“安全”的字节。绝大部分的计算机和操作系统都对 ascii 有着良好的支持,保证了编码之后的字节流能被正确地复制、传播、解析。

3 Base 128

Base 64 存在的问题就是: 编码后的每一个字节的最高两位总是 0,在不考虑 pad 的情况下,有效 bit 只占 bit 总数的 75%,造成大量的空间浪费。

是否可以进一步提高信息密度呢?

意识到这一点,你就很自然能想象出 Base 128 的大致实现思路了:将字节流按 7 bits 进行分组,然后低位补 0。但问题来了: Base 64 实际上用了 64+1 个 ASCII 字符,按照这个思路 Base 128 需要使用 128+1 个 ASCII 个字符,但是 ASCII 字符一共只有 128 个。

另外: 即使不考虑 pad,ascii 中包含了一些不可以正常打印的控制字符,编码之后的字符还可能包含会被不同操作系统转换的换行符号(10 和 13)。因此,Base 64 至今依然没有被 Base 128 替代。

Base 64 的规则因为上述限制不能完美地扩展到 Base 128,所以现有基于 Base 64 扩展而来的编码方式大部分都属于变种:如 LEB128(Little-Endian Base 128)、 Base 85 (Ascii 85),以及本文的主角:Base 128 Varints

4 Base 128 Varints

4.1 基本概念

Base 128 Varints 是 Google 开发的序列化库 Protocol Buffers 所用的编码方式。

以下为 Protobuf 官方文档中对于 Varints 的解释:

Varints are a method of serializing integers using one or more bytes. Smaller numbers take a smaller number of bytes.

即: 使用一个或多个字节对整数进行序列化,小的数字占用更少的字节。简单来说,Base 128 Varints 编码原理就是尽量只储存整数的有效位,高位的 0 尽可能抛弃。

Base 128 Varints 有两个需要注意的细节:

  • 只能对一部分数据结构进行编码,不适用于所有字节流(当然你可以把任意字节流转换为 string,但不是所有语言都支持这个 trick)。否则无法识别哪部分是无效的 bits;
  • 编码后的字节可以不存在于 ASCII 表中,因为和 Base 64 使用场景不同,不用考虑是否能正常打印;

4.2 例子

对于Base 128 Varints 编码后的每个字节,低 7 位用于储存数据,最高位用来标识当前字节是否是当前整数的最后一个字节,称为最高有效位(most significant bit, 简称msb)。msb 为 1 时,代表着后面还有数据;msb 为 0 时代表着当前字节是当前整数的最后一个字节。

下图是编码后的整数300: 第一个字节的 msb 为 1,最后一个字节的 msb 为 0。

10101100  00000010

要将这两个字节解码成整数,需要三个步骤:

  1. 去除 msb;
  2. 将字节流逆序(msb 为 0 的字节储存原始数据的高位部分,小端模式);
  3. 最后拼接所有的 bits;
- 10101100  00000010
-  0101100   0000010
-  0000010   0101100
-  00000100101100
-  300(integer)

4.3 对整数进行编码

具体过程是:

  1. 将数据按每 7 bits 一组拆分;
  2. 逆序每一个组;
  3. 添加 msb;
-  124856(integer)
-  111 1001111 0111000
-  0000111  1001111  0111000
-  0111000  1001111  0000111
- 10111000 11001111 00000111

需要注意的是: 无论是编码还是解码,逆序字节流这一步在机器处理中实际是不存在的,机器采用小端模式处理数据,此处逆序仅是为了符合人的阅读习惯而写出。

5 Protobuf编码

Protobuf支持数据类型及其编码方式:

IDNameUsed For
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enum
1I64fixed64, sfixed64, double
2LENstring, bytes, embedded messages, packed repeated fields
3SGROUPgroup start (deprecated)
4EGROUPgroup end (deprecated)
5I32fixed32, sfixed32, float

5.1 有符号整型

按照刚才变长编码的思想,-2147483646使用的比特位应该比-2要少。然而我们知道在计算机世界中负数使用补码表示的,也就是说最高位(最左侧的比特位)一定是1,假设我们使用64位来表示数字,那么如果我们依然用补码来表示数字的话那么无论这个负数有多大还是多小都需要占据10个字节的空间。

为什么是10个字节呢?

不要忘了varint每个字节的有效负荷是7个比特,那么对于需要64位表示的数字来说就需要64/7向上取整也就是10个字节来表示。这显然不能满足我们对数字变长存储的要求。

该怎么解决这个问题呢?

既然无符号数字可以方便的进行变长编码,那么我们将有符号数字映射称为无符号数字不就可以了,这就是所谓的ZigZag编码

ZigZag编码就像这样:

原始信息      编码后
0            0 
-1           1 
1            2
-2           3
2            4
-3           5
3            6
 
...          ...
 
2147483647   4294967294
-2147483648  4294967295

ZigZag编码规则:

(n << 1) ^ (n >> 31)  # for 32-bit signed integer
(n << 1) ^ (n >> 63)  # for 64-bit signed integer

5.2 定长数据

Protobuf中定长数据直接采用小端模式储存,不作转换。

5.3 字符串

以字符串"testing"为例,编码为16进制后结果如下:

07  74 65 73 74 69 6e 67
  • 第一个字节表示字符串采用 UTF-8 编码后字节流的长度(bytes),采用 Base 128 Varints 进行编码;
  • 后面字符串用UTF-8编码后的字节流;

5.4 字段类型和字段名称

字段类型有限可以用简单的3个比特位来表示(一共六种编码方式),有意思的是字段名称该怎么表示?

既然通信双方需要协议,那么某个字段其实是Client和Server都知道的,它们唯一不知道的就是“哪些值属于哪些字段”。为解决这个问题,我们给每个字段都进行编号,如protobuf消息定义:

syntax = "proto3";
 
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

这里的等号并不是用于赋值,而是给每一个字段指定一个 ID,称为 field number。消息内同一层次字段的 field number 必须各不相同。

一个键值key,在 protobuf 源码中被称为 tag,tag 由 field number 和 type 两部分组成:

  • field number 左移 3 bits;
  • 在最低 3 bits 写入 wire type(字段类型);

源码中生成的 tag 是 uint64,代表着 field number 可以使用 61 个 bit 吗?

并非如此!事实上: tag 的长度不能超过 32 bits,意味着 field number 的最大取值为 2 29 − 1 ( 536870911 ) 2^{29}-1 (536870911) 2291(536870911)

而且在这个范围内,有一些数是不能被使用的:

  • 0 :protobuf 规定 field number 必须为正整数;
  • 19000~19999: protobuf 仅供内部使用的保留位;

理解了生成 tag 的规则之后,不难得出以下结论:

  • field number 不必从1开始,可以从合法范围内的任意数字开始;
  • 不同字段间的field number不必连续,只要合法且不同即可;

但是实际上: 大多数人分配 field number 还是会从 1 开始,因为 tag 最终要经过 Base 128 Varints 编码,较小的 field number 有助于压缩空间,field number 为 1 到 15 的 tag 最终仅需占用一个字节。

当你的 message 有超过 15 个字段时,Google 也不建议你将 1 到 15 立马用完。如果你的业务日后有新增字段的可能,并且新增的字段使用比较频繁,你应该在 1 到 15 内预留一部分供新增的字段使用

当你修改的 proto 文件需要注意:

  • field number 一旦被分配了就不应该被更改,除非你能保证所有的接收方都能更新到最新的 proto 文件;
  • 由于 tag 中不携带 field name 信息,更改 field name 并不会改变消息的结构;

发送方认为的 apple 到接受方可能会被识别成 pear。双方把字段读取成哪个名字完全由双方自己的 proto 文件决定,只要字段的 wire type 和 field number 相同即可。由于 tag 中携带的类型是 wire type,不是语言中具体的某个数据结构,而同一个 wire type 可以被解码成多种数据结构,具体解码成哪一种是根据接收方自己的 proto 文件定义的。

5.5 嵌套消息

嵌套消息的实现并不复杂。在 protobuf 的 wire type 中,wire type2 (length-delimited)不仅支持 string,也支持 embedded messages。

对于嵌套消息: 首先你要将被嵌套的消息进行编码成字节流,然后你就可以像处理 UTF-8 编码的字符串一样处理这些字节流:在字节流前面加入使用 Base 128 Varints 编码的长度即可。

一个字段可以理解为K-V格式,嵌套消息可以理解为K-(K-(K-…V))

5.6 重复消息编码规则

假设接收方的 proto3 中定义了某个字段(假设 field number=1),当接收方从字节流中读取到多个 field number=1 的字段时,会执行 merge 操作。

merge 的规则如下:

  • 如果字段为不可分割的类型,则直接覆盖;
  • 如果字段为 repeated,则 append 到已有字段;
  • 如果字段为嵌套消息,则递归执行 merge;

如果字段的 field number 相同但是结构不同,则出现 error。

5.7 字段顺序

编码结果与字段顺序无关

Proto 文件中定义字段的顺序与最终编码结果的字段顺序无关,两者有可能相同也可能不同。

当消息被编码时,Protobuf 无法保证消息的顺序,消息的顺序可能随着版本或者不同的实现而变化。任何 Protobuf 的实现都应该保证字段以任意顺序编码的结果都能被读取。

以下是使用Protobuf时的一些常识:

  • 序列化后的消息字段顺序是不稳定的;
  • 对同一段字节流进行解码,不同实现或版本的 Protobuf 解码得到的结果不一定完全相同(bytes 层面),只能保证相同版本相同实现的 Protobuf 对同一段字节流多次解码得到的结果相同;
  • 假设有一条消息foo,有几种关系可能是不成立的;

相等消息编码后结果可能不同

假设有两条逻辑上相等的消息,但是序列化之后的内容(bytes 层面)不相同,原因有很多种可能。

比如下面这些原因:

  • 其中一条消息可能使用了较老版本的 protobuf,不能处理某些类型的字段,设为 unknwon;
  • 使用了不同语言实现的 Protobuf,并且以不同的顺序编码字段;
  • 消息中的字段使用了不稳定的算法进行序列化;
  • 某条消息中有 bytes 类型的字段,用于储存另一条消息使用 Protobuf 序列化的结果,而这个 bytes 使用了不同的 Protobuf 进行序列化;
  • 使用了新版本的 Protobuf,序列化实现不同;
  • 消息字段顺序不同;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/717292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【跨域认证】详解JWT,JWT是什么?

JSON Web Token&#xff08;缩写 JWT&#xff09;是目前最流行的跨域认证解决方案&#xff0c;本文介绍它的原理和用法。 一、跨域认证的问题 互联网服务离不开用户认证。一般流程是下面这样。 1、用户向服务器发送用户名和密码。 2、服务器验证通过后&#xff0c;在当前对话&…

[SSM]MyBatis使用javassist生成类和接口代理机制

目录 六、使用javassist生成类 6.1Javassist的使用 6.2使用Javassist生成DaoImpl类 七、MyBatis中接口代理机制及使用 7.1在之前的web应用中使用接口代理机制 7.2使用接口代理机制完成之前的CRUD(部分代码) 六、使用javassist生成类 6.1Javassist的使用 引入javassist依…

王道考研计算机网络第五章知识点汇总

5.1.1 传输层概述 复用&#xff1a;好比家里面每个人都要写信&#xff0c;向信箱里面投入信件&#xff0c;然后由邮递员取走。 分用&#xff1a;就是每个人都收到了各自的回信&#xff0c;然后从信箱中取走各自的信 5.2 UDP协议 注意&#xff1a;用户数据报和检验和都是指的整…

数学建模——插值(下)

本文是面向数学建模准备的&#xff0c;是介绍性文章&#xff0c;没有过多关于原理的说明&#xff01;&#xff01;&#xff01; 目录 一、2维插值原理及公式 1、二维插值问题 2、最邻近插值 3、分片线性插值 4、双线性插值 5、二维样条插值 二、二维插值及其Matlab工具箱…

记录一次Android侧滑需求代码

点击/滑动界面显示&#xff0c;不多说&#xff0c;上代码&#xff0c;性能未知 效果图 点击/滑动前界面 滑动后效果 布局 <?xml version"1.0" encoding"utf-8"?> <androidx.appcompat.widget.LinearLayoutCompat xmlns:android"…

【Cache】Squid代理服务器应用

文章目录 一、Squid 服务器的概念1. 代理服务器概述CDN 服务器 2. 代理的工作机制3. Squid 服务器的作用4. Squid 代理的类型 二、部署 Squid 服务器1. 安装 Squid 服务1.1 编译安装 Squid1.2 修改 Squid 的配置文件1.3 Squid 的运行控制1.4 创建 Squid 服务脚本1.5 supervisor…

在k8s集群中部署一个应用程序

一、 k8s集群简单介绍 上图描述的是拥有一个Master(主)节点和六个Worker(工作)节点的k8s集群 Master 负责管理集群 负责协调集群中的所有活动&#xff0c;例如调度应用程序&#xff0c;维护应用程序的状态&#xff0c;扩展和更新应用程序。 Worker节点(即图中的Node)是VM(虚…

模拟CSRF攻击

今天给大家表演一个拙劣的CSRF攻击。 我会编写两个应用&#xff1a;一个是正经应用&#xff0c;一个是钓鱼的应用。然后让后者攻击前者&#xff0c;让它打钱&#xff01; 一、绪论 1.1 先聊聊Cookie 参考&#xff1a;常用的本地存储——cookie篇 Cookie在八股文里面好像已…

模板类的开发

模板类的开发 栈定长数组变长数组 栈 入栈和出栈使用引用是为了传递参数 注意构造函数初始化列表使用模板的写法 注意析构函数delete指针需要 [ ] 测试 定长数组 重载了括号运算符 调用的其实是数组 使用int 使用char 变长数组

比亚迪车载Android开发岗三面经历~

前言 首先&#xff0c;我想说一下我为什么会想去比亚迪这样的车企做车载Android开发。我是一名有5年经验的Android开发工程师&#xff0c;之前一直在互联网软件公司工作&#xff0c;做过移动端App和IoT产品的开发。但我一直对汽车领域很感兴趣&#xff0c;也希望自己的技术能应…

TOT(Tree of Thought) | GPT-4+dfs搜索算法提升大模型复杂问题解决能力

大家好&#xff0c;我是HxShine。 今天分享一篇普林斯顿大学的一篇文章&#xff0c;Tree of Thoughts: Deliberate Problem Solving with Large Language Models[1]&#xff1a;思维之树:用大型语言模型解决复杂问题。 这篇工作还是非常有借鉴意义的&#xff0c;OpenAI的Andr…

2023年07月在线IDE流行度最新排名

点击查看最新在线IDE流行度最新排名&#xff08;每月更新&#xff09; 2023年07月在线IDE流行度最新排名 TOP 在线IDE排名是通过分析在线ide名称在谷歌上被搜索的频率而创建的 在线IDE被搜索的次数越多&#xff0c;人们就会认为它越受欢迎。原始数据来自谷歌Trends 如果您相…

深度学习基础

1 机器学习、深度学习、人工智能 1.1 机器学习 机器学习是一门专门研究计算机怎样模拟或实现人类的学习行为&#xff0c;以获取新的知识或技能&#xff0c;重新组织已有的知识结构使之不断改善自身性能的学科。 基本步骤&#xff1a;获取数据、数据预处理、特征提取、特征选择…

postman几种常见的请求方式

1、get请求直接拼URL形式 对于http接口&#xff0c;有get和post两种请求方式&#xff0c;当接口说明中未明确post中入参必须是json串时&#xff0c;均可用url方式请求 参数既可以写到URL中&#xff0c;也可写到参数列表中&#xff0c;都一样&#xff0c;请求时候都是拼URL 2&am…

【win怎么给两个屏幕设置不同壁纸】

在现在经济的发展下&#xff0c;多数用户都拥有了两个屏幕&#xff0c;那么如何在不适用壁纸软件的情况下&#xff0c;将两个屏幕设置成不同的壁纸呢&#xff1f; 操作 首先将需要进行设置的图片选好&#xff0c;将其保存到桌面上&#xff0c;紧接着框选两张图片&#xff0c;…

探索Gradio库中的Image模块及其强大功能

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

84、基于stm32单片机超市自助存储柜快递箱系统设计(程序+原理图+流程图+参考论文+开题报告+任务书+设计资料+元器件清单等)

单片机主芯片选择方案 方案一&#xff1a;AT89C51是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS型8位单片机&#xff0c;器件采用ATMEL公司的高密度、非易失性存储技术生产&#xff0c;兼容标准MCS-51指令系统&#xff0c;片内置通用8位中央处理器(CPU)和Flash存储单元&a…

Go程序结构- package和import

1、包和文件 在Go语言中包的作用和其他语言中的库或模块的作用类似&#xff0c;用于支持模块化、封装、编译隔离和重用。关键点如下&#xff1a; (1)包中保存一个或者多个.go结尾的文件&#xff0c;而包的目录就是包的导入路径 (2)中Go中通过一条简单的规则来管理标识符是否对外…

下个版本已定!C++自救新动作!

自去年年底&#xff0c;美国安全局&#xff08;NSA&#xff09;在其所发布的《Software Memory Safety》报告中点名批评C之后&#xff0c;C之父Bjarne Stroustrup一顿回怼后&#xff0c;做出决定&#xff1a;内部自救。现在&#xff0c;就让我们看看下一个版本的C&#xff0c;究…

【Go】Go 语言教程--语言变量(五)

往期教程&#xff1a; Go 语言教程–介绍&#xff08;一&#xff09;Go 语言教程–语言结构&#xff08;二&#xff09;Go 语言教程–语言结构&#xff08;三&#xff09;Go 语言教程–数据类型&#xff08;四&#xff09; 文章目录 变量声明多变量声明值类型和引用类型简短形…