ProtoBuf:序列化与反序列化、基础数据结构、关键词、数据格式

ProtoBuf

第一阶段:实现TCP客户端服务器框架

第二阶段:在TCP客户端服务器框架基础上实现HTTP服务框架

第三阶段:在TCP客户端服务器框架基础上实现基于Protobuf的RPC框架

1.ProtoBuf:序列化反序列化的库

  • 另一种当前比较流行的序列化方式:json、restful

  • Protobuf库的使用

  • rpc框架是什么

  • 分布式框架思想

2.TCP客户端服务器框架基础上实现RPC框架

基础认识

ProtoBuf--->Protocol Buffers

  • Google开发的一种与语言无关、平台无关、可拓展的序列化结构化数据方法

  • 可以理解成一种更小、更快、更简单的XML或JSON替代平品

  • 适合数据存储、网络通信(常与gRPC搭配)等高性能和强数据结构的场景

核心概念与特点

  • 数据描述语言数据结构的定义具体实现语言分离 ,存放在独立的.proto文件中。

  • 代码生成机制 :通过专用的编译器 (protoc)读取.proto定义,自动 生成目标语言(eg:C++、Java、Python)的数据访问和序列化代码

  • 二进制序列化:序列化后的数据是紧凑的二进制格式,不具有可读性,但对机器非常高效

  • 向后/向前兼容 :可以修改.proto文件 (如添加新字段)而不会破坏旧版本的程序旧代码忽略新字段新代码 读取旧数据 时也会为新字段设置默认值 。这是通过唯一的字段编号实现的,所以在字段定义好后请勿修改其编号

序列化与反序列化

序列化:将多个数据对象按照指定的格式进行组织,组织成为可持久化存储或网络传输的二进制数据串

反序列化:将二进制数据串按照指定格式进行解析,得到各个数据对象过程

序列化方式的性能指标

序列化后的数据长度:长度决定持久化所占的空间大小,以及传输速度

与其他序列化方式的对比

典型的两种序列化与反序列化方式

JOSN序列化
  • 在C++中有一个JSON序列化库:libjsoncpp-dev
ProtoBuff序列化

安装:

bash 复制代码
sudo apt-get install libprotobuf-dev protobuf-compiler
  • sudo:以超级用户(root)权限执行命令,系统安装软件时必须使用该权限。

  • apt-get install:Debian/Ubuntu 系统的软件包管理命令,用于从源仓库下载并安装指定的软件。

  • libprotobuf-dev :Protobuf 的 C++ 开发头文件与库文件 (例如 google/protobuf/*.h)。在编译依赖 Protobuf 的 C++ 项目时需要用到它。

  • protobuf-compiler :Protobuf 的 命令行编译器(protoc 。用于将 .proto 数据定义文件编译转换为 C++、Python、Java 等语言的代码。

.proto文件的编写

基础数据类型

  • 变长:将固定长度数据--->不固定长编码 ,主要用于进行空间上的优化

  • 定长:基础数据类型本身所占空间大小不会改变,进行原始数据的编码

  • eg:数据num=1;定长占用4B,变长可能只需要1B

bash 复制代码
syntax="proto3";//声明使用proto3的语法版本
package person;//包名--->对应c++的命名空间--避免命名冲突
​
message students
{
    int32 sn=1;
    string name=2;
}
​
//protoc --cpp_out=./ person.proto

关键词

关键词 作用
syntax 指定 Protobuf 语法版本
package 定义 Protobuf 包名
option 配置编译器或生成代码
message 定义结构化数据
enum 定义枚举
service 定义 RPC 服务
rpc 定义 RPC 方法
repeated 定义数组/列表
map 定义 Map
oneof 多选一字段
optional 可选字段
reserved 保留字段编号/名称
import 导入其他 .proto

repeated

  • 定义动态数组/列表 ,相当于C++中的std::vector

假设字段定义为:repeated int32 ids = 4;repeated Score scores = 5;

  • 元素数量与清空

    • int ids_size() const:获取元素总数

    • void clear_ids():清空整个列表

  • 基础类型列表 (repeated int32)

    • int32_t ids(int index) const:按索引读取

    • void set_ids(int index, int32_t value):按索引修改

    • void add_ids(int32_t value):末尾追加新元素

  • Message 类型列表 (repeated Score)

    • const Score& scores(int index) const:按索引只读读取

    • Score* mutable_scores(int index):按索引获取指定位置可修改指针

    • Score* add_scores():在末尾自动 new 一个新对象并返回其指针

  • 获取原生容器指针

    • const google::protobuf::RepeatedField& ids() const

    • google::protobuf::RepeatedPtrField* mutable_scores():拿到底层的集合指针,用于标准迭代器遍历或批量操作

Message 结构体全局通用方法

  • void Clear():重置所有字段到初始状态

  • bool SerializeToString(std::string* output) const:序列化二进制流写入 string

  • bool ParseFromString(const std::string& data):从二进制流读取并解析

  • size_t ByteSizeLong() const:计算序列化后的二进制字节总长度

  • std::string DebugString() const:将结构体内容格式化输出为可读文本

  • void CopyFrom(const MyMessage& from):深拷贝另一个同类型 Message 对象

map

  • 用于存储键值对**(Key-Value)**的数据结构

    • Key:必须是整数、bool、string

    • Value:可以是任意标量类型、枚举或Message(但不能嵌套另一个map)

    • 修饰符:map字段不能用repeated修饰

enum

  • 枚举类型

复制代码
enum Color
{
    RED=0;
    BLUE=1;
    GREEN=3;
}
  • 枚举的第一个值必须是0

oneof

  • 多个字段中最多只能有一个生效

核心区别对比

对比维度 C/C++ 原生 union Protobuf oneof
状态追踪 。不记录当前存的是哪个成员,需开发者手动维护 enum 标记 。内部自动维护 oneof_case() 枚举,可精确查询当前激活的字段
复杂类型支持 极其有限。传统 union 无法直接容纳包含构造/析构函数的类型(如 std::string 完全支持 。可安全修饰 stringbytes 及嵌套 Message
内存管理 手动。切换成员时若涉及动态内存,需显式调用析构函数与 placement new 自动 。给新字段赋值时,Protobuf 会自动析构并释放旧字段的内存
读写安全性 未定义行为 (UB)。若写入成员 A 却按成员 B 读取,会发生二进制错乱或崩溃 类型安全。读取未激活的字段时返回该类型的默认初始值,不会崩溃
序列化 仅为 C/C++ 内存布局,无内置序列化机制 原生支持打包为紧凑二进制流,且仅序列化当前生效的字段
  • 状态判定 :调用 oneof_case() 方法会返回 ..._NOT_SET(数值对应 0),调用内部各字段的 has_xxx() 均返回 false

    读取数据 :直接调用内部字段的 getter 方法不会报错或崩溃 ,而是安全地返回对应类型的默认零值 (例如 int 返回 0string 返回 "")。

    序列化传输 :在将 Message 序列化为二进制流(SerializeAsString())时,Protobuf 会完全跳过oneof 字段,不占用任何字节空间

optional

  • 可选字段,提供存在性检测

bash 复制代码
syntax = "proto3";
​
message Student {
    int32 age = 1;            // 普通标量:无 has_age()
    optional int32 score = 2; // 显式 optional:生成 has_score()
}
维度 Protobuf optional C++17 std::optional
空值表达 has_score() == false score == std::nullopt
未赋值时读取 返回默认值 0(不会崩溃) *opt 未定义行为 / value() 抛异常
状态存储 集中位图存储(内存占用极低) 独立的包装结构体(存在内存对齐 Padding)
C++ 标准依赖 C++11 及以上 C++17 及以上

bytes

类型 含义
bytes 任意长度原始二进制字节流,0 字节、1 字节、10MB 都可以存。存二进制,不会做字符串编码转换。
string UTF‑8 字符串文本,只能存合法文本,不能随便存二进制。
uint8 / fixed8 单个字节,1 字节整数

数据格式说明

字段格式

定长:TV

不定长:TLV(Tag Length Value)

Tag

Varint编码的整数,由字段编号+数据类型组成

Wire Type 名称 对应数据类型
0 Varint int32, int64, uint32, bool, enum, sint32
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, 嵌套 Message, packed repeated 数组
5 32-bit fixed32, sfixed32, float
大小
字段编号 (Field Number) 范围 Tag 占用大小 占用字节数说明
1 ~ 15 1 字节 包含 3 位 Wire Type + 4 位 Field Number
16 ~ 2,047 2 字节 包含 3 位 Wire Type + 11 位 Field Number
2,048 ~ 262,143 3 字节 包含 3 位 Wire Type + 18 位 Field Number
262,144 ~ 2\^{29}-1 4 ~ 5 字节 Protobuf 支持的最大字段编号上限为 536,870,911

最佳实践

  • 优先给高频字段分配 1~15 :将 1 到 15 的编号留给重复出现(repeated)或者业务中最核心、频繁传输的字段,这能为大规模 RPC 通信节省大量网络带宽。

  • 保留 19000~19999 :Protobuf 内部实现保留了 1900019999 的字段编号,请勿在定义中使用。

二进制存储 时,单个字节 采用 Varint 规则,最高位(MSB)用于标识后续字节,每个字节仅有 7 位 可用于存储有效数值:

  • 低 3 位 :固定分配给 wire_type(表示数据格式,范围 0~7)。

  • 高 4 位 :分配给 field_number

field_number 为 1~15 时,\\text{field_number} \\ll 3 的最大值为 15 \\times 8 = 120,加上 wire_type 后整体数值依然小于 128(2\^7),因此 仅需 1 个字节 即可装下。

相关推荐
witton10 天前
xmake中将proto文件生成C/C++文件然后再编译链接进项目
c语言·c++·mingw·protobuf·xmake·protobuf-c·protoc-gen-c
寒水馨1 个月前
Linux下载、安装protobuf-v35.1(附安装包protoc-35.1-linux-x86_64.zip)
linux·运维·服务器·google·序列化·protobuf·protoc
寒水馨1 个月前
macOS下载、安装protobuf-v35.1(附安装包protoc-35.1-osx-universal_binary.zip)
macos·google·grpc·序列化·protobuf·protoc·数据交换
乱七八糟的屋子1 个月前
FlatBuffers C++ 实战教程:从零到一掌握高性能序列化
c++·零拷贝·序列化·flatbuffers c++
想你依然心痛2 个月前
数据序列化:Protobuf-nano与MessagePack在嵌入式中的对比——编解码与内存占用实战
序列化·messagepack·protobuf-nano
佛祖让我来巡山2 个月前
Java中的序列化和反序列化:让你的对象学会“变身术”
序列化·反序列化·java 序列化 反序列化
yinchnag3 个月前
Protobuf 存负数为什么这么费——sint32 与 ZigZag 编码
protobuf
葱卤山猪3 个月前
二进制字节流序列化
c++·序列化
yinchnag3 个月前
proto协议统计带宽字节流量
protobuf