Protobuf原理与序列化

本文目录

  • 1. Protobuf介绍
  • 2. Protobuf的优势
  • 3. 编写Protobuf
    • 头部全局定义
    • 消息结构具体定义
    • 字段类型定义
    • 标签号
    • Base128编码
  • 4. TLV
    • Protobuf的TLV编码
    • 如何通过Varint表示300?
  • 5. 编译Protobuf
  • 6. 构造消息对象

前言:之前写项目的时候只是简单用了下Protobuf,以为就弄懂protobuf了,今天刚好跟朋友聊天,被朋友拷打知不知道Protobuf原理,ok,确实不是很懂, 找了一些文章看看来搞懂,于是就有了这篇文章。

1. Protobuf介绍

在网络通信和数据存储的时候,数据序列化 是非常重要的,特别是现在微服务横行,序列化更是至关重要。传统HTTP通信的时候,一般都是用Json作为消息传递的数据格式。但是谷歌一直在用Protobuf,这肯定是有原因的,所以特意学习了一下Protobuf,来研究研究。

Protobuf(Protocol Buffers)是由 Google 开发的一种轻量级、高效的数据交换格式,它被用于结构化数据的序列化、反序列化和传输。相比于 XML 和 JSON 等文本格式,Protobuf 具有更小的数据体积、更快的解析速度和更强的可扩展性。

核心思想:使用协议(Protocol)来定义数据的结构和编码方式。使用 Protobuf,可以先定义数据的结构各字段的类型字段等信息,然后使用Protobuf提供的编译器生成对应的代码,用于序列化和反序列化数据。由于 Protobuf 是基于二进制编码的,因此可以在数据传输和存储中实现更高效的数据交换,同时也可以跨语言使用。

比如下面这张图,就是很好的一个例子。Java语言写序列化,然后接收端用Python进行反序列化。

在这里插入图片描述

2. Protobuf的优势

更小的数据量:Protobuf 的二进制编码通常比 XML 和 JSON 小 3-10 倍,因此在网络传输和存储数据时可以节省带宽和存储空间。

更快的序列化和反序列化速度:由于 Protobuf 使用二进制格式,所以序列化和反序列化速度比 XML 和 JSON 快得多。

跨语言:Protobuf 支持多种编程语言,可以使用不同的编程语言来编写客户端和服务端。这种跨语言的特性使得 Protobuf 受到很多开发者的欢迎(JSON 也是跨语言的)。

易于维护可扩展:Protobuf 使用 .proto 文件定义数据模型和数据格式,这种文件比 XML 和 JSON 更容易阅读和维护,且可以在不破坏原有协议的基础上,轻松添加或删除字段,实现版本升级和兼容性。

3. 编写Protobuf

// 文件:addressbook.proto
syntax = "proto3";// 指定 Protobuf 包名,防止有相同类名的message定义
package goprotobuf;// Go 生成的包路径 可以通过 go_package 选项指定
option go_package = "/";message Person {// =1,=2 作为序列化后的二进制编码中的字段的唯一标签,也因此,1-15 比 16 会少一个字节,所以尽量使用 1-15 来指定常用字段。optional int32 id = 1;optional string name = 2;optional string email = 3;enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {optional string number = 1;optional PhoneType type = 2;}repeated PhoneNumber phones = 4;
}message AddressBook {repeated Person people = 1;
}

头部全局定义

syntax = "proto3":指定 Protobuf 版本为版本3(最新版本)
option go_package = "/": Go 生成的包路径,可以通过 go_package 选项指定

消息结构具体定义

message Person 定一个了一个 Person 类。

其中:
修饰符 optional 表示可选字段,可以不赋值。
修饰符 repeated 表示数据重复多个,如数组,如 List。
修饰符 required 表示必要字段,必须给值,否则会报错 RuntimeException,但是在 Protobuf 版本 3 中被移除。

字段类型定义

修饰符后面跟着的是 字段类型,比如 string字符串、bytes二进制数据类型、enum枚举类型,message消息类型,可以嵌套其他的消息类型。bool布尔类型,只有两个值,true和false。

标签号

字段后面的 =1 这种 是作为 序列化之后的 二进制编码 中的 字段 的对应标签。因为protobuf消息在序列化之后是不包含字段信息的,只有对应的字段序号,所以节省了对应的空间。

尽量使用1-15编号,比16少一个字节,这里我们来讲讲为什么。

Base128编码

Protobuf 使用一种称为 Base 128 编码(也称为 LEB128 或 Protobuf 的 Varint 编码)来表示字段标签号和字段值。这种编码方式会根据数值的大小动态分配字节数,以节省空间。具体规则如下:

字段标签号的编码:

字段标签号在序列化时会被编码为 Varint 格式。Varint 编码是一种可变长度的编码方式,小数值占用的字节数更少。

Varint 编码的规则:

如果数值小于 128,占用 1 个字节。

如果数值大于等于 128,会占用多个字节(每个字节的最高位为 1,最后一个字节的最高位为 0)。

字段标签号的计算:

在 Protobuf 中,字段标签号会与字段类型信息结合,形成一个 Key,用于标识字段。
Key 的计算公式为:Key = (FieldNumber << 3) | WireType

<<3称为 左移运算符(Left Shift Operator),它将一个二进制数的所有位向左移动指定的位数,并在右侧填充零。具体来说,<<3 表示将一个数的二进制表示向左移动 3 位。

也就是说,左移 n 位的效果相当于将原数乘以2^n

其中,FieldNumber 是字段的标签号,WireType 是字段类型的编码(例如,0 表示 Varint 类型,2 表示 Length-Delimited 类型等)。

在这里插入图片描述

| WireType 是指 加上这个数值,比如:Key=(FieldNumber×8)+WireType

也就是标签号为 15 的字段,Key = (15 << 3) | 0 = 120

这些值都小于 128,因此可以使用 1 个字节 来表示。

因为 FieldNumber 是一个整数,而 WireType 的范围是 0 到 7,所以 FieldNumber 需要左移 3 位(即乘以 8),以便为 WireType 留出低 3 位的空间(这样就刚好能够容纳0-7,从二进制的角度来说)。这样,Key 值可以同时包含字段编号和字段类型的信息。


可能有很多人很好奇,1个字节应该可以表示0-255,而不是128.这里我们继续来看看。

在计算机中,一个字节(Byte)由 8 个位(Bit)组成。每个位可以是 0 或 1,因此一个字节可以表示 2^8=256 种不同的值,范围从 0 到 255。

但是在Protobu f的 Varint 编码中,一个字节可以表示的最大数值是 127,而不是 255。这是因为 Varint 编码使用最高位(即第 8 位)作为 继续位(Continuation Bit),用于指示是否还有更多的字节跟随。

如果最高位为 0,表示该字节是最后一个字节;如果最高位为 1,表示后面还有更多的字节。

所以当表示127的时候,就是 0111 1111 ,也就是120+7=127

在二进制中,1000 0000 表示的十进制数值是 128。但在 Protobuf 的 Varint 编码中,这个二进制数的最高位是 1,表示它不是最后一个字节,后面还有更多的字节。因此,1000 0000 在 Varint 编码中表示的数值是 128,但它是多字节序列的一部分,而不是单独的一个字节。也就是它表示一个数值为 128 的多字节序列的开始

那么,1000 0000 0000 0000为128吗?并不是。

再来总结下Varint编码的规则:每个字节的低 7 位用于存储数据,每个字节的最高位(第 8 位)用于表示是否还有后续字节:如果最高位是 1,表示后面还有更多字节。如果最高位是 0,表示这是最后一个字节。


这也就是为什么说明了 因此,使用 1-15 的标签号可以减少序列化后的数据大小,尤其是在消息中包含大量字段时,这种节省会更明显。这也是为什么 Protobuf 推荐将常用字段的标签号放在 1-15 的范围内。

4. TLV

TLV 是一种编码结构,用于描述数据的组织方式。TLV 是 Tag-Length-Value 的缩写,表示数据由三部分组成。

Tag(标签):用于标识字段的编号和类型。在 Protobuf 中,Tag 是由字段编号(field number)和线缆类型(wire type)组合而成的,通过公式 (field_number << 3) | wire_type 编码。

Length(长度)表示 Value 部分的长度。对于某些数据类型(如字符串、嵌套消息等,即string、bytes、embedded messages),Length 是必要的;而对于一些固定长度的类型(如 int32、fixed64 等),Length 可能会被省略。

Value(值):是实际存储的数据内容

比如

message Person {int32 id = 1;string name = 2;
}

对应的实例为

id: 123
name: "Alice"

其二进制编码可能如下:

08 7B:08 是 Tag(字段编号 1,类型为 VARINT),7B 是 Value(123 的 Varint 编码),int类型不需要显示指定长度。

对于存储Varint编码数据,就不需要存储字节长度 Length,所以实际上Protocol Buffer的存储方式是 T - V;

12 05 41 6C 69 63 65:12 是 Tag(字段编号 2,类型为 LEN),05 是 Length(5,表示字符串长度),41 6C 69 63 65 是 Value(字符串 “Alice” 的 UTF-8 编码)。

若Protocol Buffer采用其他编码方式(如LENGTH_DELIMITED)则采用T - L - V


Protobuf的TLV编码

Protobuf 在将数据转换成二进制时,会对字段和类型重新编码,减少空间占用。它采用 TLV 格式来存储编码后的数据。TLV 也是就是 Tag-Length-Value ,是一种常见的编码方式,因为数据其实都是键值对形式,所以在 TAG 中会存储对应的字段和类型信息,Length 存储内容的长度,Value 存储具体的内容。

上面我们讲过,比如类型信息标记,比如 int32 怎么标记,因为类型个数有限,所以 Protobuf 规定了每个类型对应的二进制编码,比如 int32 对应二进制 000,string 对应二进制 010,这样就可以只用三个比特位存储类型信息。

这种编码方式可以在数据值比较小的情况下,只使用一个字节来存储数据,以此来提高编码效率。

并且Protobuf 还可以通过采用压缩算法来减少数据传输的大小。比如 GZIP 算法能够将原始数据压缩成更小的二进制格式,从而在网络传输中能够节省带宽和传输时间。Protobuf 还提供了一些可选的压缩算法,如 zlib 和 snappy,这些算法在不同的场景下能够适应不同的压缩需求

比如下面这张图。

在这里插入图片描述
在这里插入图片描述

根据刚刚的公式来解释下。

首先是id的Tag:1<<3 + 2= 10(注意id是string类型) ,也就是 1010,。

然后是name的Tag:2 << 3 + 2 = 18,也就是10010

Length长度就更好理解了,分别是1和2。

如何通过Varint表示300?

在这里插入图片描述

5. 编译Protobuf

使用 Protobuf 提供的编译器,可以将 .proto 文件编译成各种语言的代码文件(如 Java、C++、Python 等)。

在这里插入图片描述
比如下面两种编译代码方式。

protoc --java_out=./java ./resources/addressbook.protoprotoc --go_out=./go

6. 构造消息对象

刚刚我们定义了对应Proto消息对象如下,那么我们应该怎么使用。

syntax = "proto3";// 指定 Protobuf 包名,防止有相同类名的message定义
package goprotobuf;// Go 生成的包路径 可以通过 go_package 选项指定
option go_package = "/";message Person {// =1,=2 作为序列化后的二进制编码中的字段的唯一标签,也因此,1-15 比 16 会少一个字节,所以尽量使用 1-15 来指定常用字段。optional int32 id = 1;optional string name = 2;optional string email = 3;enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {optional string number = 1;optional PhoneType type = 2;}repeated PhoneNumber phones = 4;
}message AddressBook {repeated Person people = 1;
}

这里给出对应的Go版本代码方式。

package mainimport ("fmt""log""github.com/wdbyte/protobuf/addressbook" // 假设这是生成的 Go 包路径
)func main() {// 直接构建phoneNumber1 := &addressbook.PhoneNumber{Number: "18388888888",Type:   addressbook.PhoneType_HOME,}person1 := &addressbook.Person{Id:    1,Name:  "www.wdbyte.com",Email: "xxx@wdbyte.com",Phones: []*addressbook.PhoneNumber{phoneNumber1},}addressBook1 := &addressbook.AddressBook{People: []*addressbook.Person{person1},}fmt.Println(addressBook1)fmt.Println("------------------")// 链式构建addressBook2 := &addressbook.AddressBook{People: []*addressbook.Person{{Id:    2,Name:  "www.wdbyte.com",Email: "yyy@126.com",Phones: []*addressbook.PhoneNumber{{Number: "18388888888",Type:   addressbook.PhoneType_HOME,},},},},}fmt.Println(addressBook2)
}

输出如下:

people {id: 1name: "www.wdbyte.com"email: "xxx@wdbyte.com"phones {number: "18388888888"type: HOME}
}------------------
people {id: 2name: "www.wdbyte.com"email: "yyy@126.com"phones {number: "18388888888"type: HOME}
}

参考文章:
1、https://blog.csdn.net/carson_ho/article/details/70568606/?ops_request_misc=&request_id=&biz_id=102&utm_term=Varint%E7%BC%96%E7%A0%81%E5%A6%82%E4%BD%95%E8%A1%A8%E7%A4%BA128%EF%BC%9F&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-5-70568606.142^v101^pc_search_result_base5&spm=1018.2226.3001.4187
2、https://segmentfault.com/a/1190000043775488#item-4-5

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

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

相关文章

DeepSeek:面向效率与垂直领域的下一代大语言模型技术解析

本文将深入剖析DeepSeek模型的核心算法架构&#xff0c;揭示其在神经网络技术上的突破性创新&#xff0c;并与主流大模型进行全方位技术对比。文章涵盖模型设计理念、训练范式优化、应用场景差异等关键维度&#xff0c;为读者呈现大语言模型领域的最新发展图景。 一、DeepSeek…

数据安全_笔记系列09_人工智能(AI)与机器学习(ML)在数据安全中的深度应用

数据安全_笔记系列09_人工智能&#xff08;AI&#xff09;与机器学习&#xff08;ML&#xff09;在数据安全中的深度应用 人工智能与机器学习技术通过自动化、智能化的数据分析&#xff0c;显著提升了数据分类、威胁检测的精度与效率&#xff0c;尤其在处理非结构化数据、复杂…

【Python 语法】Python 数据结构

线性结构&#xff08;Linear Structures&#xff09;1. 顺序存储列表&#xff08;List&#xff09;元组&#xff08;Tuple&#xff09;字符串&#xff08;String&#xff09; 2. 线性存储栈&#xff08;Stack&#xff09;队列&#xff08;Queue&#xff09;双端队列&#xff08…

docker本地镜像源搭建

最近Deepseek大火后&#xff0c;接到任务就是帮客户装Dify&#xff0c;每次都头大&#xff0c;因为docker源不能用&#xff0c;实在没办法&#xff0c;只好自己搭要给本地源。话不多说具体如下&#xff1a; 1、更改docker的配置文件&#xff0c;添加自己的私库地址&#xff0c…

Ae 效果详解:粒子运动场

Ae菜单&#xff1a;效果/模拟/粒子运动场 Simulation/Particle Playground 粒子运动场 Particle Playground效果可以用于创建和控制粒子系统&#xff0c;模拟各种自然现象&#xff0c;如烟雾、火焰、雨水或雪等。通过调整粒子的发射点、速度、方向和其他属性&#xff0c;可以精…

CSS 对齐:深入理解与技巧实践

CSS 对齐:深入理解与技巧实践 引言 在网页设计中,元素的对齐是至关重要的。一个页面中元素的对齐方式直接影响到页面的美观度和用户体验。CSS 提供了丰富的对齐属性,使得开发者可以轻松实现各种对齐效果。本文将深入探讨 CSS 对齐的原理、方法和技巧,帮助开发者更好地掌握…

汽车无钥匙进入一键启动操作正确步骤

汽车智能无钥匙进入和一键启动的技术在近年来比较成熟&#xff0c;不同车型的操作步骤可能略有不同&#xff0c;但基本的流程应该是通用的&#xff0c;不会因为时间变化而有大的改变。 移动管家汽车一键启动无钥匙进入系统通常是通过携带钥匙靠近车辆&#xff0c;然后触摸门把…

Android之APP更新(通过接口更新)

文章目录 前言一、效果图二、实现步骤1.AndroidManifest权限申请2.activity实现3.有版本更新弹框UpdateappUtilDialog4.下载弹框DownloadAppUtils5.弹框背景图 总结 前言 对于做Android的朋友来说&#xff0c;APP更新功能再常见不过了&#xff0c;因为平台更新审核时间较长&am…

AI触手可及 | 基于函数计算玩转AI大模型

AI触手可及 | 基于函数计算玩转AI大模型 基于函数计算部署AI大模型的优势方案架构图像生成 - Stable Diffusion WebUI部署操作 释放资源部署总结体验反馈 在生成式AI技术加速迭代的浪潮下&#xff0c;百亿级参数的行业大模型正推动产业智能化范式转移。面对数字化转型竞赛&…

DDD该怎么去落地实现(4)多对多关系

多对多关系的设计实现 如题&#xff0c;DDD该如何落地呢&#xff1f;前面我通过三期的内容&#xff0c;讲解了DDD落地的关键在于“关系”&#xff0c;也就是通过前面我们对业务的理解先形成领域模型&#xff0c;然后将领域模型的原貌&#xff0c;形成程序代码中的服务、实体、…

【补阙拾遗】排序之冒泡、插入、选择排序

炉烟爇尽寒灰重&#xff0c;剔出真金一寸明 冒泡排序1. 轻量化情境导入 &#x1f30c;2. 边界明确的目标声明 &#x1f3af;3. 模块化知识呈现 &#x1f9e9;&#x1f4ca; 双循环结构对比表★★★⚠️ 代码关键点注释 4. 嵌入式应用示范 &#x1f6e0;️5. 敏捷化巩固反馈 ✅ …

前端面试题---小程序跟vue的声明周期的区别

1. 小程序生命周期 小程序的生命周期主要分为 页面生命周期 和 应用生命周期。每个页面和应用都有自己独立的生命周期函数。 应用生命周期 小程序的应用生命周期函数与全局应用相关&#xff0c;通常包括以下几个钩子&#xff1a; onLaunch(options)&#xff1a;应用初始化时触…

【芯片设计】NPU芯片前端设计工程师面试记录·20250227

应聘公司 某NPU/CPU方向芯片设计公司。 小声吐槽两句,前面我问了hr需不需要带简历,hr不用公司给打好了,然后我就没带空手去的。结果hr小姐姐去开会了,手机静音( Ĭ ^ Ĭ )面试官、我、另外的hr小姐姐都联系不上,结果就变成了两个面试官和我一共三个人在会议室里一人拿出…

让Word插上AI的翅膀:如何把DeepSeek装进Word

在日常办公中&#xff0c;微软的Word无疑是我们最常用的文字处理工具。无论是撰写报告、编辑文档&#xff0c;还是整理笔记&#xff0c;Word都能胜任。然而&#xff0c;随着AI技术的飞速发展&#xff0c;尤其是DeepSeek的出现&#xff0c;我们的文字编辑方式正在发生革命性的变…

点击修改按钮图片显示有问题

问题可能出在表单数据的初始化上。在 ave-form.vue 中&#xff0c;我们需要处理一下从后端返回的图片数据&#xff0c;因为它们可能是 JSON 字符串格式。 vue:src/views/tools/fake-strategy/components/ave-form.vue// ... existing code ...Watch(value)watchValue(v: any) …

vue深拷贝:1、使用JSON.parse()和JSON.stringify();2、使用Lodash库;3、使用深拷贝函数(采用递归的方式)

文章目录 引言三种方法的优缺点在Vue中,实现数组的深拷贝I JSON.stringify和 JSON.parse的小技巧深拷贝步骤缺点:案例1:向后端请求路由数据案例2: 表单数据处理时复制用户输入的数据II 使用Lodash库步骤适用于复杂数据结构和需要处理循环引用的场景III 自定义的深拷贝函数(…

线性模型 - 支持向量机

支持向量机&#xff08;SVM&#xff09;是一种用于分类&#xff08;和回归&#xff09;的监督学习算法&#xff0c;其主要目标是找到一个最佳决策超平面&#xff0c;将数据点分为不同的类别&#xff0c;并且使得分类边界与最近的数据点之间的间隔&#xff08;margin&#xff09…

记录一次解决springboot需要重新启动项目才能在前端界面展示静态资源的问题--------使用热部署解决

问题 使用sprinbootthymeleaf&#xff0c;前后端不分离&#xff0c;一个功能是用户可以上传图片&#xff0c;之后可以在网页展示。用户上传的图片能在对应的静态资源目录中找到&#xff0c;但是在target目录没有&#xff0c;导致无法显示在前端界面 解决 配置热部署 <depe…

【Python pro】函数

1、函数的定义及调用 1.1 为什么需要函数 提高代码复用性——封装将复杂问题分而治之——模块化利于代码的维护和管理 1.1.1 顺序式 n 5 res 1 for i in range(1, n1):res * i print(res) # 输出&#xff1a;1201.1.2 抽象成函数 def factorial(n):res 1for i in range(1…

[Web 信息收集] Web 信息收集 — 手动收集 IP 信息

关注这个专栏的其他相关笔记&#xff1a;[Web 安全] Web 安全攻防 - 学习手册-CSDN博客 0x01&#xff1a;通过 DNS 服务获取域名对应 IP DNS 即域名系统&#xff0c;用于将域名与 IP 地址相互映射&#xff0c;方便用户访问互联网。对于域名到 IP 的转换过程则可以参考下面这篇…