【RPC】—Protobuf编码原理

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.hqwc.cn/news/14027.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASL-QPSO|改进量子粒子群自适应算法及其实现(Matlab)

作者在前面的文章中介绍了量子粒子群算法&#xff0c;量子粒子群算法不但继承粒子群算法的优点&#xff0c;还有它自身计算模型更加简洁&#xff0c;控制参数更少等更加突出的优势&#xff0c;但依然存在着一定的局限性。 例如也会存在着早熟收敛的问题&#xff0c;随着迭代次数…

JAVA每日一练(1)

【程序1】 题目&#xff1a;古典问题&#xff1a;有一对兔子&#xff0c;从出生后第3个月起每个月都生一对兔子&#xff0c;小兔子长到第三个月后每个月又生一对兔子&#xff0c;假如兔子都不死&#xff0c;问每个月的兔子对数为多少&#xff1f; import java.util.Scanner;/*…

封装一个带el-form的,带el-table的,带分页的,带搜索查询的dialog组件,很使用的二次封装组件。

#封装dialog小案例 提示&#xff1a;这是我工作中封装的代码&#xff0c;很使用&#xff0c;需要的可以拿去&#xff0c; 在我们的代码中往往会出现点击按钮出现弹窗进行操作&#xff0c;那么我们就需要对dialog进行一个二次封装。 下边是大概的一个样式。 ##对组件进行二次…

【雕爷学编程】Arduino动手做(156)---OTTO两足舵机机器人

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

虹科教程 | Linux网络命名空间与虹科PROFINET协议栈的GOAL中间件结合使用

前言 PROFINET是由PI推出的开放式工业以太网标准&#xff0c;它使用TCP/IP等IT标准&#xff0c;并由IEC 61158和IEC 61784 标准化&#xff0c;具有实时功能&#xff0c;并能够无缝集成到现场总线系统中。凭借其技术的开放性、灵活性和性能优势&#xff0c;PROFINET可应用于过程…

网络数据包的监听与分析——IP数据报文分析

1. 抓包工具下载 x下面是一个IP数据报的抓包软件——IPtool的蓝奏云下载链接 https://wwix.lanzoue.com/iaGpy11klpnc 2. iptool使用 下载解压之后&#xff0c;右击以管理员身份运行&#xff0c;打开该exe文件即可 然后点击绿色运行就开始捕包了 随便点一个包进去进行分析就可…

指针和数组笔试题解析

目录 数组笔试题 一维数组 字符数组 题 一 题 二 题 三 题 四 题 五 题 六 二维数组 指针笔试题 笔试题一 笔试题二 笔试题三 笔试题四 笔试题五 笔试题六 笔试题七 本篇博文&#xff0c;将从指针和数组来为大家分析一些笔试题&#xff0c;设计内…

【霹雳吧啦Wz】Transformer中Self-Attention以及Multi-Head Attention详解

文章目录 来源Transformer起源Self-Attention1. 求q、k、v2. 计算 a ^ ( s o f t m a x 那块 ) \hat{a} (softmax那块) a^(softmax那块)3. 乘V&#xff0c;计算结果 Multi-Head Attention位置编码 来源 b站视频 前天啥也不懂的时候点开来一看&#xff0c;各种模型和公式&#…

pycharm 打开终端,安装第三方程序

鼠标移动到左下角 弹出列表&#xff0c;选择终端&#xff0c;当然也可以用快捷键唤出&#xff0c; 可以输入命令进行第三方库的安装

EMQ X(3):客户端websocket消息收发

在EMQ X Broker提供的 Dashboard 中 TOOLS 导航下的 Websocket 页面提供了一个简易但有效的WebSocket 客户端工具&#xff0c;它包含了连接、订阅和发布功能&#xff0c;同时还能查看自己发送和接收的报文数据&#xff0c;我们期望 它可以帮助您快速地完成某些场景或功能的测试…

基于深度学习的高精度球场足球检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度球场足球检测识别系统可用于日常生活中或野外来检测与定位球场足球目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的球场足球目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5…

开启Windows共享文件夹审核,让用户查看谁删除了文件

在动画行业有个常用到的需求&#xff0c; 我的共享文件夹内的文件被谁删除了&#xff0c;查不到&#xff0c;只能查看谁创建&#xff0c;谁修改的&#xff0c;但查不到谁删除的&#xff0c;分享一下&#xff1a; 1 开始->运行->gpedit.msc 开发本地组策略编辑器, 在计算…