Protocol Buffers设计要点

news2024/9/21 2:34:51

概述

一种开源跨平台的序列化结构化数据的协议。可用于存储数据或在网络上进行数据通信。它提供了用于描述数据结构的接口描述语言(IDL),也提供了根据 IDL 产生代码的程序工具。Protocol Buffers的设计目标是简单和性能,所以与 XML 相比更小且更快。在 Google,它被当作一个 RPC 系统的基础。

设计过程

Tag 表示

对于一条 person 信息,使用 JSON 可表示为:{ “age”: 30, “name”: “zhangsan”, “height”: 175.33, “weight”: 140 }。显然,中间有很多冗余的字符,比如 {," 等,为把数据变小一点,我们直接将其表示为:30|zhangsan|175.33|140,即通过直接将 value 使用分隔符 | 拼在一起,舍去了不必要的冗余字符(每条记录都要包含各个字段的字段名很浪费存储空间),这样大幅度地压缩了空间。然后接收端按照如下顺序解析 key-value 之间的关系。

字段1:age字段2:name字段3:height字段4:weight
30zhangsan175.33140

假设 height 这个字段为 null,我们其实是不必要传递这个字段的,这个时候我们需要传递的数据就为:30|zhangsan|140。但是在接收端,解析数据并按照顺序进行字段匹配的时候就会出问题。

字段1:age字段2:name字段3:height字段4:weight
30zhangsan140

显然已经乱套了,为了保证能够正确的配对,我们可以使用 tag 技术,即每个字段我们都用tag|value 的方式来存储的,在 tag 当中记录两种信息:

  • value 对应的字段的编号
  • value 的数据类型

因为 tag 中有字段编号信息,所以即使没有传递 height 字段的 value 值,根据编号也能正确的配对。

使用 tag 也会增加额外的空间,这跟 JSON 的 key/value 有什么区别吗? JSON 中的 key 是字符串,每个字符就会占据一个字节,像 name 这个 key 就会占据 4 个字节,但在 PB 中,tag 使用二进制进行存储,一般只会占据 1 个字节,它的代码为:

static int makeTag(final int fieldNumber, final int wireType) {
  return (fieldNumber << 3) | wireType;
}
  • fieldNumber 就是字段编号,比如 fieldNumber 为 1 表示age,为 2 表示 name 等;
  • wireType 表示字段的数据类型,以此来计算 value 占用字节的大小。

注:在 protobuf 当中,wireType可以支持的字段类型如下:
在这里插入图片描述

编码优化

我们知道整数在计算机当中占 4 个字节,但绝大部分的整数,比如价格,库存等,都是比较小的整数,实际用不了 4 个字节,比如 127 在计算机中的二进制为:

00000000 00000000 00000000 01111111

完全可以用最后 1 个字节来进行存储,protobuf 当中定义了 Varint 这种数据类型,可以用不同的长度来存储整数,进一步压缩数据的存储空间。

另外在计算机中,负数使用补码表示,比如 -1,它的二进制表示方式为:

11111111 11111111 11111111 11111111

显然就无法用 1 个字节来表示了,这个时候就可以使用 zigzag 算法对负数进行变换,最终可用 2 个字节来表示 -1。

Varints

《深入理解 Kafka:核心设计与实践原理》

Varints 是使用一个或多个字节来序列化整数的一种方法。数值越小,其占用的字节数就越少。Varints 中的每个字节都有一个位于最高位的 msb 位(most significant bit),除最后一个字节外,其余 msb 位都设置为 1,最后一个字节的 msb 位为 0。这个 msb 位表示其后的字节是否和当前字节一起来表示同一个整数。 除 msb 位外,剩余的 7 位用于存储数据本身,这种表示型又称为 Base 128。通常而言,一个字节 8 位可以表示 256 个值,所以称为 Base 256,而这里只能用 7 位表示,2 的 7 次方即 128。Varints 中采用的是小端字节序,即最小的字节放在最前面。

举个例子,比如数字 1,它只占一个字节,所以 msb 位为 0:

0000 0001

再举一个复杂点得例子,比如数字 300:

1010 1100 0000 0010

300 得二进制表示原本为 0000 0001 0010 1100 = 256 + 32 +8 + 4 = 300。那么为什么 300 的变长表示为上面这种形式呢?

首先去掉每个字节的 msb 位,表示为:

1010 1100 0000 0010 -> 010 1100 000 0010(varints 表示去掉 msb 的结果)

如前所述,varints 使用小端字节序的布局方式,所以这里两个字节的位置需要翻转再补上最高位:

010 1100 000 0010
-> 000 0010 010 1100(翻转)
-> 0000 0010 0010 1100(补上每字节的最高位 0)

Varints 可以用来表示 int32、int64、uint32、uint64、sint32、sint64、bool、enum 等类型。在实际使用过程中,如果当前字段可以表示为负数,那么对 int32/int64 和 sint32/sint64 而言,它们在进行编码时存在较大的区别。比如使用 int64 表示一个负数,那么哪怕是 -1(-1 正常需要 8 * 8 = 64 位来表示,而 varint 表示法,每个字节的最高位需要作为 msb,相当于每个字节只有 7 位,所以至少需要 10 字节(10 * 7 > 64 > 9 * 7)来表示),其编码后的长度始终为 10 个字节,就如同对待一个很大的无符号长整型数一样。为了使编码更加高效,Varints 使用了 ZigZag 的编码方式。

ZigZag 编码以一种锯齿形(zig-zags)的方式来回穿梭正负整数,将带符号整数映射为无符号整数,这样可以使绝对值较小的负数仍然享有较小的 Varints 编码值,比如 -1 编码为 1,1 编码为 2,-2 编码为 3,如表5-1所示。
ZigZag编码
对应的公式为:

(n << 1) ^ (n >> 31) (sin32)、(n << 1) ^ (n >> 63)(sin64)

以 -1 为例, 其二进制表现形式为 1111 1111 1111 1111 1111 1111 1111 1111 ( 补码 )。

( n << 1 ) = 1111 1111 1111 1111 1111 1111 1111 1110
(n >> 31) = 1111 1111 1111 1111 1111 1111 1111 1111
(n << 1) ^ (n >> 31 ) = 1

最终 -1 的 Varints 编码为 0000 0001 ,这样原本用 4 个字节表示的 -1 现在可以用 1 个字节来表示了。

前面说过 Varints 中的一个字节中只有 7 位是有效数值位, 即 只能表示 128 个数值 ,转变成绝对值之后其实质上只能表示 64 个数值 。 比如对消息体长度而言,其值肯定是大于等于 0 的正整数,那么一个字节长度的 Varints 最大只能表示 64 。 65 的二进制数表示为:

0100 0001

经过 ZigZag 处理后为:

1000 00 1 0 A 0000 0000 = 1000 0010

每个字节的低 7 位是有效数值位,所以 1000 0010 进一步转变为:

000 0001 000 0010

而 Varints 使用小端字节序,所以需要翻转一下位置:

000 0010 000 0001

设置非最后一个字节的 msb 位为 1 ,最后一个字节的 msb 位为 0,最终有:

1000 0010 0000 0001

所以最终 65 表示为 1000 0010 0000 0001 ,而 64 却表示为 0 100 0000 。

解码

因为每个字段都用 tag|value 来表示,在 tag 中包含了 value 的数据类型信息,所以可以直接从 tag 中得知 value 的字节大小,从而快速地解析出数据。例如 value 是 bool 型,我们就知道其占了 1 个字节,程序从 tag 后面直接读一个字节就可以解析出 value,而 JSON 则需要进行字符串解析才可以办到。

如果 value 是字符串类型的,我们无法从 tag 当中得知 value 的实际长度,就不得不做字符串匹配操作,要知道字符串匹配是非常耗时的。 为了能够快速解析字符串类型的数据,protobuf 在存储的时候,对其做了特殊的处理,即其存储被分做三部分:tag|length|value,其中的 leg 记录了字符串的长度,同样使用了 varint 来存储,然后程序从 leg 后截取 leg 个字节的数据作为 value。

在这里插入图片描述

协议比较

Avro、ProtoBuf、Thrift 的模式演进之法【翻译】

如果有一些数据,想存储在文件中,或者通过网络发送出去,对数据的处理主要分为如下几个阶段:

  1. 使用编程语言内置的序列化机制,如 Java Serialization、 Ruby Marshal 或 Python pickle,或者甚至发明自己的格式。
  2. 意识到被锁定在一种编程语言中是很糟糕的,所以转向使用一种广泛支持的、与语言无关的格式,比如 JSON (或者 XML)。
  3. 觉得 JSON 过于冗长,解析速度太慢。也会恼火它竟然无法区分整数和浮点。所以出现了一种类似于 JSON 的格式,但它是二进制的。(比如: MessagePack、 BSON、Binary JSON 等)。
  4. 当使用不一致的协议类型进行通信时,总会出现对象字段匹配失效的问题。此外,类似于 JSON 的二进制文件并没有那么紧凑,因为它们仍会一遍又一遍地存储字段名。如果有一个协议,可以避免存储对象的字段名,则可以节省更多字节。可用选项通常是 Thrift, Protocol Buffers(以下简称 ProtoBuf)或 Avro。这三种序列化协议都是基于模式设计的,为 Java 开发人员提供了高效的、跨语言数据序列化和代码生成能力。

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

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

相关文章

vue脚手架创建项目:账号登录(利用element-ui快速开发)(取消eslint强制格式)(修改端口号)

新手看不懂&#xff0c;老手不用看系列 文章目录 一、准备工作1.1 取消强制格式检查1.2 导入依赖&#xff0c;注册依赖 二、去element-ui官网找样式写Login组件2.1 引用局部组件2.2 运行项目 三、看一下发现没问题&#xff0c;开始修改前端的代码四、修改端口号4.1 修改后端端口…

Redis 之死:Garantia Data 如何策划了开源史上最大劫案?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Temple of Doom靶场nodejs获取shellss-manager漏洞tcpdump提权

下载链接&#xff1a; Temple of Doom: 1 ~ VulnHub 下载完成后直接在vxbox中导入即可&#xff0c;网络链接模式根据自身情况而定&#xff08;我采用的桥接模式&#xff09; 正文&#xff1a; 先用nmap进行扫描靶机ip nmap -sn 192.168.1.1/24 对192.168.1.5进行端口探测&a…

Spring学习——什么是循环依赖及其解决方式

文章目录 前言一、什么是循环依赖二、解决思路1、循环依赖分类2、对象初始化步骤及对象分类3、spring是如何解决的4、图解5、三级缓存1、区别2、ObjectFactory是什么 三、源码debug1、spring创建对象过程1、dubug第一步——找到getBean2、dubug第二步——getBean与doGetBean3、…

基于AT89C51单片机的智能交通灯设计

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/89035863?spm1001.2014.3001.5503 1绪 论 1.1课题研究背景 交通是城市经济活动的命脉&#xff0c;对城市经济发展、人民生活水平的提高起着十分重要的作用。城市交…

用 C++ 编码架构图的最佳用例

统一建模语言&#xff08;UML&#xff09;&#xff0c;作为一种实际应用的语言标准&#xff0c;借助一系列架构图呈现建模软件系统。UML 的出现鼓励了自动化软件工具的开发&#xff0c;有助于自动代码生成。UML 图面向对象系统和软件工具&#xff0c;将静态结构和动态行为以可视…

如何统计代码量

工具&#xff1a; cloc 下载地址&#xff1a; Releases AlDanial/cloc GitHub 使用方法&#xff1a;

2024.3.26

实现闹钟 weiget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTimer> #include<QTime> #include<QTimerEvent> #include<QString> #include<QtTextToSpeech> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } Q…

力扣● 503.下一个更大元素II ● 42. 接雨水

503.下一个更大元素II 与496.下一个更大元素 I的不同是要循环地搜索元素的下一个更大的数。那么主要是对于遍历结束后&#xff0c;单调栈里面剩下的那些元素。 如果直接把两个数组拼接在一起&#xff0c;然后使用单调栈求下一个最大值就可以。 代码实现的话&#xff0c;不用直…

ubuntu22.04配置Azure Kinect DK深度相机

一.安装SDK 今天我来配置一下微软公司的Azure Kinect DK深度相机,以前在ubuntu18.04上配置过,因为官方说唯一支持linux版本是18.04,所以在18.04中配置还算顺利 but这不代表不可以在更高版本的ubuntu中使用,只不过需要自己去多配置一些东西 apt 源安装 更新源: c…

上海企业必应bing国内广告推广如何开户?

随着数字化营销时代的深入发展&#xff0c;搜索引擎广告已成为众多企业提升品牌知名度和促进产品销售的重要手段之一。在国内市场&#xff0c;微软必应&#xff08;Bing&#xff09;搜索广告以其精准定位与高价值用户群赢得了众多企业的青睐。对于位于上海地区的企业来说&#…

蓝桥杯:Python基础学习一

目录 一、遍历列表 1.使用for 循环和 enumerate()函数实现 2.案例代码 二、对列表进行统计和计算 1.统计数值列表的元素和 2.案例代码 三、对列表进行排序 1.使用列表对象的sort()方法 2.使用内置的 sorted()函数实现 四、列表推导式 1.从列表中选择符合条件的元素组…

flask_restful结合蓝图使用

在蓝图中&#xff0c;如果使用 Flask_RESTful &#xff0c; 创建 Api 对象的时候&#xff0c;传入蓝图对象即可&#xff0c;不再是传入 app 对象 /user/__init__.py from flask.blueprints import Blueprintuser_bp Blueprint(user,__name__)from user import views /user…

泛型编程的启蒙之旅

个人主页&#xff1a;日刷百题 系列专栏&#xff1a;〖C/C小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 &#x1f30e;欢迎各位→点赞&#x1f44d;收藏⭐️留言&#x1f4dd; ​ ​ 讲模板之前呢&#xff0c;我们先来谈谈泛型编程&#xff1a; 泛型编程&#xff1a;编写与类…

Linux——命名管道

Linux——命名管道 命名管道命名管道和匿名管道的区别 创建命名管道利用命名管道实现简单通信 我们之前学习了匿名管道&#xff0c;这种管道有一个缺点就是只有两个有血缘关系的进程才能够使用匿名管道&#xff0c;这个非常不方便。所以我们又在匿名管道的基础之上引入了命名管…

Linux实现m4a格式转换为wav格式

需要在linux上安装ffmpeg 参考博客 Linux上安装ffmpeg修改环境变量【这一点很重要&#xff0c;自己因为没有添加环境变量&#xff0c;捣鼓了很长时间】 将ffmpeg的绝对路径添加到 PATH 环境变量中&#xff0c;以让系统能找到ffmpeg的安装路径。 # /home//project/ffmpeg-6.1-a…

QT作业day3

1、使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;密码是…

vue3+threejs新手从零开发卡牌游戏(八):关联卡组和手牌区、添加初始化卡组和初始化手牌逻辑

首先我们优化下之前的代码&#xff0c;先加载游戏资源&#xff0c;然后再初始化场景&#xff0c;由于目前只有一个font字体需要加载&#xff0c;所以我们将之前game/deck/p1.vue中的font相关代码迁移到game/index.vue下&#xff0c;同时使用async和await处理异步加载&#xff0…

干货分享之反射笔记

入门级笔记-反射 一、利用反射破泛型集合二、Student类三、获取构造器的演示和使用1.getConstructors只能获取当前运行时类的被public修饰的构造器2.getDeclaredConstructors:获取运行时类的全部修饰符的构造器3.获取指定的构造器3.1得到空构造器3.2得到两个参数的有参构造器&a…

基于java+springboot+vue实现的超市管理系统(文末源码+Lw+ppt)23-354

摘 要 系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对超市管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”…