【RPC】—Protobuf编码原理

这篇具有很好参考价值的文章主要介绍了【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~😊

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



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编码对照表如下图:

【RPC】—Protobuf编码原理,RPC,rpc,网络协议,网络

对照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支持数据类型及其编码方式:

ID Name Used For
0 VARINT int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 I64 fixed64, sfixed64, double
2 LEN string, bytes, embedded messages, packed repeated fields
3 SGROUP group start (deprecated)
4 EGROUP group end (deprecated)
5 I32 fixed32, 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 层面)不相同,原因有很多种可能。

比如下面这些原因:文章来源地址https://www.toymoban.com/news/detail-535772.html

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

到了这里,关于【RPC】—Protobuf编码原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 在protobuf里定义描述rpc方法的类型

    service  UserServiceRpc     //在test.proto中定义 {     rpc Login(LoginRequest)returns(LoginResponse);     rpc GetFriendLists(GetFriendListRequest)returns(GetFriendListResponse); }   test.proto文件生成test.pb.cc      protoc  test.proto  --cpp_out=./    将生成的文件放到 ./ 目录下,截取一部分如下 调用关系如图所示

    2024年04月25日
    浏览(50)
  • 10 - 网络通信优化之通信协议:如何优化RPC网络通信?

    微服务框架中 SpringCloud 和 Dubbo 的使用最为广泛,行业内也一直存在着对两者的比较,很多技术人会为这两个框架哪个更好而争辩。 我记得我们部门在搭建微服务框架时,也在技术选型上纠结良久,还曾一度有过激烈的讨论。当前 SpringCloud 炙手可热,具备完整的微服务生态,

    2024年02月11日
    浏览(38)
  • rpc入门笔记 0x02 protobuf的杂七杂八

    安装grpcio和grpcio-tools库 生成proto的python文件 python -m grpc_tools.protoc :使用grpc_tools包中的protoc命令进行代码生成。 --python_out=. :指定生成的Python代码的存放位置为当前目录。 --grpc_python_out=. :指定生成的gRPC代码的存放位置为当前目录。 -I. :指定搜索.proto文件的路径为当前目录

    2024年02月08日
    浏览(59)
  • RPC原理与Go RPC详解

    RPC(Remote Procedure Call),即远程过程调用。它允许像调用本地服务一样调用远程服务。 RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。 首先与RPC(远程过程调用)相对应的是本地调用。 本地调用 将上述程序编译成二

    2024年02月14日
    浏览(33)
  • RPC和HTTP协议

            RPC 全称(Remote Procedure Call),它是一种针对跨进程或者跨网络节点的应用之间的远程过程调用协议。         它的核心目标是,让开发人员在进行远程方法调用的时候,就像调用本地方法一样,不需要额外为了完成这个交互做过的编码。         为了达到

    2024年02月11日
    浏览(41)
  • 自定义Dubbo RPC通信协议

    Dubbo 协议层的核心SPI接口是 org.apache.dubbo.rpc.Protocol ,通过扩展该接口和围绕的相关接口,就可以让 Dubbo 使用我们自定义的协议来通信。默认的协议是 dubbo,本文提供一个 Grpc 协议的实现。 Google 提供了 Java 的 Grpc 实现,所以我们站在巨人的肩膀上即可,就不用重复造轮子了。

    2024年01月19日
    浏览(54)
  • 【Go】四、rpc跨语言编程基础与rpc的调用基础原理

    早期 Go 语言不使用 go module 进行包管理,而是使用 go path 进行包管理,这种管理方式十分老旧,两者最显著的区别就是:Go Path 创建之后没有 go.mod 文件被创建出来,而 go module 模式会创建出一个 go.mod 文件用于管理包信息 现在就是:尽量使用 Go Modules 模式 另外,我们在引入包

    2024年02月19日
    浏览(42)
  • 安卓协议逆向 咸鱼 frida rpc 调用方案

    通过frida rpc调用真机获取指定的搜索结果数据。 本文仅供大家学习及研究使用、切勿用于各种非法用途。 frida 提供了一种跨平台的 rpc (远程过程调用)机制,通过 frida rpc 可以在主机和目标设备之间进行通信,并在目标设备上执行代码,可实现功能如下: 1、动态地

    2024年02月07日
    浏览(41)
  • RPC核心原理详解

    RPC的全称是Remote Procedure Call,即远程过程调用。简单解读字面上的意思,远程肯定是指要跨机器而非本机,所以需要用到网络编程才能实现,但是不是只要通过网络通信访问到另一台机器的应用程序,就可以称之为RPC调用了?显然并不够。 我理解的RPC是帮助我们屏蔽网络编程

    2024年02月11日
    浏览(36)
  • 【原理】RPC与HTTP

    RPC服务基本架构包含了四个核心的组件,分别是Client,Server,Clent Stub以及Server Stub。 RPC 让远程调用就像本地调用一样,其调用过程可拆解为以下步骤。 ① 服务调用方(client)以本地调用方式调用服务; ② client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消

    2024年04月14日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包