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) |
完全支持 。可安全修饰 string、bytes 及嵌套 Message |
| 内存管理 | 手动。切换成员时若涉及动态内存,需显式调用析构函数与 placement new | 自动 。给新字段赋值时,Protobuf 会自动析构并释放旧字段的内存 |
| 读写安全性 | 未定义行为 (UB)。若写入成员 A 却按成员 B 读取,会发生二进制错乱或崩溃 | 类型安全。读取未激活的字段时返回该类型的默认初始值,不会崩溃 |
| 序列化 | 仅为 C/C++ 内存布局,无内置序列化机制 | 原生支持打包为紧凑二进制流,且仅序列化当前生效的字段 |
-
状态判定 :调用
oneof_case()方法会返回..._NOT_SET(数值对应0),调用内部各字段的has_xxx()均返回false。读取数据 :直接调用内部字段的 getter 方法不会报错或崩溃 ,而是安全地返回对应类型的默认零值 (例如
int返回0,string返回"")。序列化传输 :在将 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 内部实现保留了
19000到19999的字段编号,请勿在定义中使用。
在二进制存储 时,单个字节 采用 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 个字节 即可装下。