前面我们已经在Linux网络中学习了序列化相关的内容,并且利用JsonCpp三方库进行序列化和反序列化。本期我们就来介绍另一个适用于序列化的库ProtoBuf
相关代码以及提交到作者的个人gitee:ProtoBuf代码: 本代码仅仅只是我用来学习ProtoBuf第三方库的代码仓库
目录
[核心基础:.proto 文件标准结构](#核心基础:.proto 文件标准结构)
[标量数据类型(proto3 ↔ C++ 类型映射)](#标量数据类型(proto3 ↔ C++ 类型映射))
[字段规则(proto3 核心:取消 required)](#字段规则(proto3 核心:取消 required))
[Map 类型(原生键值对)](#Map 类型(原生键值对))
[Oneof 互斥字段(节省传输体积)](#Oneof 互斥字段(节省传输体积))
[Package 与 Import(模块化开发)](#Package 与 Import(模块化开发))
[Reserved 保留字段(协议兼容核心)](#Reserved 保留字段(协议兼容核心))
[选项(Option):C++ 性能优化核心](#选项(Option):C++ 性能优化核心)
[Service 服务定义(gRPC 核心)](#Service 服务定义(gRPC 核心))
[四种 RPC 调用模式](#四种 RPC 调用模式)
[默认值规则(proto3 关键特性)](#默认值规则(proto3 关键特性))
[Protobuf3 语法禁忌(C++ 开发必记)](#Protobuf3 语法禁忌(C++ 开发必记))
[C++ 核心基础编译指令(必掌握)](#C++ 核心基础编译指令(必掌握))
[高频进阶参数(C++ 大型网络工程必备)](#高频进阶参数(C++ 大型网络工程必备))
[--proto_path / -I:指定协议搜索路径(解决 import 报错)](#--proto_path / -I:指定协议搜索路径(解决 import 报错))
[批量编译所有 proto 文件](#批量编译所有 proto 文件)
序列化与反序列化
在单机程序中,数据以内存对象的形式存在------结构体、类实例、容器等。这些对象依赖于特定的编译器实现、字节对齐方式、甚至CPU架构(大小端)。当我们需要将这些数据跨进程 (不同地址空间)、跨网络 (不同机器)、跨语言 (不同运行时)或者跨时间 (存储后将来恢复)进行交换时,就必须将内存对象转换为一种与具体环境解耦的、可传输的字节序列 。这个转换过程就是序列化 ,其逆过程称为反序列化。
总而言是:
序列化 :把对象转换为字节序列的过程 称为对象的序列化。
反序列化:把字节序列恢复为对象的过程 称为对象的反序列化
序列化方案本质上是在解决三个核心矛盾:
-
空间效率:序列化后的数据越小,网络带宽和存储成本越低。
-
时间效率:序列化/反序列化过程消耗的CPU时间越少,系统吞吐量越高。
-
可理解性与可演进性:数据是否便于人类排查问题?协议升级时,新旧版本能否共存?
目前并没有完美解决这三点的方案,主流的序列化工具有Json、ProtoBuf、XML等。
ProtoBuf的特点
ProtoBuf(Protocol Buffers)是Google设计的一种语言中立、平台无关、可扩展的二进制序列化协议,同时配套了完整的代码生成工具链。它在高性能分布式系统中被广泛采用
契约驱动的代码生成机制
ProtoBuf最显著的特点之一就是强制先定义数据契约 。你需要用.proto文件声明消息结构
bash
message User {
uint64 user_id = 1;
string name = 2;
repeated string tags = 3;
}
然后通过protoc编译器生成对应语言(C++、Java、Go、Python等)的代码。在C++中,你会得到一个类:包含set_user_id()、user_id()、add_tags()等类型安全的访问器,以及SerializeToArray()、ParseFromString()等序列化方法。
工程意义 :这种机制将"数据结构定义"与"业务代码"彻底分离。团队内部以.proto文件作为接口契约,不同语言的服务可以基于同一个.proto生成各自的对等代码,从根本上杜绝了手写解析逻辑带来的不一致和低级错误。同时,编译期类型检查保证了字段访问的合法性,避免JSON中root["age"]拼写错误导致的运行时缺陷。
ProtoBuf生成的代码提供了类型安全的接口。编译期就能发现类型不匹配。此外,.proto语法还支持枚举、嵌套消息、oneof(表示一组互斥的字段)、map类型等。你还可以通过optional显式标识某个字段可能缺失,并在业务代码中通过has_xxx()判断是否存在
bash
User user;
user.set_user_id(123); // 参数必须是 uint64_t
user.set_name("Alice"); // 参数是 const string&
user.add_tags("premium"); // 自动管理 repeated 字段
极致的空间效率
ProtoBuf采用二进制编码,完全摒弃了文本格式中的冗余字符(逗号、引号、花括号、字段名)。它的核心编码策略包括:
-
字段编号代替字段名 :每个字段在
.proto中被赋予一个唯一编号(如=1、=2),序列化时只传输这个编号(以Varint编码,小数字占1字节)和对应的值,字段名完全丢弃。 -
Varint与Zigzag编码 :整数采用可变长编码,小整数只占1字节(例如
1编码后就是0x08,而JSON中需要"1"至少1字节ASCII再加引号)。负数通过Zigzag转换后同样压缩。 -
可选字段省略 :未设置的
optional字段根本不出现在二进制流中,而JSON通常会用null或干脆缺失键,但键名本身仍会占用字节。
实测对比:对于一个包含字符串、整数的典型业务消息,ProtoBuf序列化后的体积通常是JSON的1/3到1/2,甚至更小。在千亿级流量系统中,这直接转化为带宽成本和存储成本的大幅下降。
卓越的时间效率
ProtoBuf的解析过程是纯粹的二进制解析,没有字符处理、没有字符串比较、没有动态键名查找。解析器按照预先生成的代码直接读取字节流:
-
读取一个Varint得到字段编号和类型。
-
根据编号直接跳转到对应的成员变量处理逻辑(生成代码中是
switch或跳转表)。 -
基本类型直接按字节拷贝或转换,复杂类型(如字符串、子消息)先读长度再读取指定范围的字节。
相比之下,JSON解析需要:跳过空白、解析引号、字符串匹配字段名、处理转义、数值字符串转换等等。ProtoBuf的解析效率通常比最快的JSON解析器(如RapidJSON)还要快数倍,且内存分配次数显著减少(可以预分配或者使用Arena分配器)。在高吞吐场景下(如RPC每秒处理百万级请求),这些CPU周期的节省非常可观。
内部服务间通信,首推ProtoBuf;对外接口,JSON和ProtoBuf可以共存(网关做转换)
下载ProtoBuf
验证安装
| 验证项 | 命令 | 预期结果 |
|---|---|---|
| 编译器版本 | protoc --version |
显示版本号 |
| 编译器位置 | which protoc |
显示可执行文件路径 |
| 动态库链接 | `ldconfig -p | grep protobuf` |
Windows
官方网站:Releases · protocolbuffers/protobuf
Windows下最直接的方式是下载预编译二进制包:
-
访问GitHub Releases页面,选择适合的版本(如
protoc-21.11-win64.zip) -
将压缩包解压到指定目录,例如
D:\protobuf -
随后在环境变量中系统变量Path中新增一条记录,填入D:\protobuf\bin(以实际下载安装路径为准)
Ubuntu
bash
sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev
其中protobuf-compiler是编译器,libprotobuf-dev是C++开发库
Centos
Centos的ProtoBuf的版本相对较老
bash
sudo yum install -y protobuf-compiler protobuf-devel
ProtoBuf3语法
核心基础:.proto 文件标准结构
proto3 的协议定义文件以 .proto 为后缀,第一行必须声明语法版本,这是强制要求。
我们可以参考以下的格式
bash
// 1. 强制声明:proto3 语法(必须写在第一行)
syntax = "proto3";
// 2. 包声明:对应 C++ 命名空间,解决多文件命名冲突
package com.company.network;
// 3. 导入外部 proto 文件(模块化开发必备)
import "google/protobuf/any.proto";
// 4. 全局选项(C++ 性能优化核心)
option cc_enable_arenas = true;
option optimize_for = SPEED;
// 5. 枚举类型定义
enum Status {
STATUS_OK = 0;
STATUS_ERROR = 1;
}
// 6. 消息类型定义(核心数据结构)
message User {
int32 id = 1;
string name = 2;
repeated string emails = 3;
}
// 7. 服务定义(gRPC RPC 接口)
service UserService {
rpc GetUser(UserRequest) returns (UserResponse);
}
标量数据类型(proto3 ↔ C++ 类型映射)
标量类型是 proto3 的基础,直接对应 C++ 原生类型 ,是网络传输的最小数据单元。proto3 无 int 简写,必须指定位数;同时区分编码方式(varint 变长编码 /fixed 定长编码),直接影响序列化性能。
| Protobuf 类型 | C++ 对应类型 | 编码方式 | 说明 |
|---|---|---|---|
| double | double | fixed64 | 64 位浮点数 |
| float | float | fixed32 | 32 位浮点数 |
| int32 | int32_t | varint | 32 位有符号整数(负数效率低) |
| int64 | int64_t | varint | 64 位有符号整数(负数效率低) |
| uint32 | uint32_t | varint | 32 位无符号整数 |
| uint64 | uint64_t | varint | 64 位无符号整数 |
| sint32 | int32_t | varint | 32 位有符号整数(负数优先用) |
| sint64 | int64_t | varint | 64 位有符号整数(负数优先用) |
| fixed32 | uint32_t | fixed32 | 32 位定长无符号(数值 > 2²⁸用) |
| fixed64 | uint64_t | fixed64 | 64 位定长无符号(数值 > 2⁵⁶用) |
| sfixed32 | int32_t | fixed32 | 32 位定长有符号 |
| sfixed64 | int64_t | fixed64 | 64 位定长有符号 |
| bool | bool | varint | 布尔值 |
| string | std::string | UTF-8 | 字符串(长度不超过 2³²) |
| bytes | std::string | 原生字节 | 二进制字节流(C++ 用 string 存储) |
C++ 开发关键建议:
- 传输负数必须用 sint32/sint64,varint 编码效率提升 50%+;
- 大数值(>2²⁸)用 fixed 类型,避免变长编码的性能损耗。
字段规则(proto3 核心:取消 required)
proto3 彻底移除了 proto2 的 required 规则,仅保留两种字段规则,这是与 proto2 的最大区别:
默认规则:singular(单字段)
-
不写任何修饰符,默认规则;
-
一个消息中,该字段最多出现一次;
-
可选字段,不设置则使用默认值。
bash
message User {
int32 id = 1; // singular:单字段,默认规则
string name = 2;
}
repeated(重复字段)
-
表示数组 / 列表 ,对应 C++ 的
std::vector; -
proto3 默认开启 packed 编码(二进制紧凑打包),性能远优于 proto2;
-
支持任意标量 / 枚举 / 消息类型。
bash
message User {
repeated string emails = 3; // 字符串数组
repeated User friends = 4; // 消息数组
}
字段编号规则(性能 + 兼容核心)
-
每个字段必须分配唯一编号(1 ~ 2²⁹ - 1);
-
1~15 :占用 1 字节(高频字段优先用,C++ 序列化性能最优);
-
16~2047:占用 2 字节;
-
19000~19999:保留编号,禁止使用。
枚举类型(enum)
用于定义固定取值集合,对应 C++ 枚举类,是网络协议中状态码、类型定义的标准方案。
核心语法规则
- 必须包含 0 值枚举(proto3 强制),作为默认值;
- 枚举值必须是正整数;
- 支持嵌套枚举;
- 允许别名(需开启
allow_alias选项)。
bash
// 全局枚举
enum Status {
STATUS_OK = 0; // 强制:第一个值必须为 0
STATUS_TIMEOUT = 1;
STATUS_PARAM_ERR = 2;
}
// 嵌套枚举
message Request {
enum CmdType {
CMD_LOGIN = 0;
CMD_LOGOUT = 1;
}
CmdType cmd = 1;
Status status = 2;
}
// 带别名的枚举
enum Color {
option allow_alias = true; // 开启别名
COLOR_RED = 0;
COLOR_CRIMSON = 0; // 别名:与 RED 同值
COLOR_BLUE = 1;
}
消息类型(message)
message 是 proto3 的核心数据结构,对应 C++ 生成的类,是网络传输的最小数据单元。
基础消息
message User {
int32 id = 1;
string name = 2;
bool is_vip = 3;
}
嵌套消息
支持多层嵌套,对应 C++ 嵌套类,模块化管理数据结构:
message User {
message Address { // 嵌套消息
string province = 1;
string city = 2;
}
Address address = 4; // 使用嵌套消息
repeated Address addresses = 5; // 嵌套消息数组
}
导入外部消息
通过 import 复用其他 proto 文件的消息:
import "user.proto";
message Order {
int64 order_id = 1;
User user = 2; // 复用外部消息
}
Map 类型(原生键值对)
proto3 原生支持 map,对应 C++ 的 std::map,用于存储键值对数据,无序。
语法
map<key_type, value_type> field_name = 编号;
规则
- key 仅支持:标量数字 / 字符串(禁止枚举、消息);
- value 支持:任意类型(除 map);
- 不支持
repeated嵌套 map; - 序列化后为有序键值对,但解析后无序。
示例
message User {
map<int32, string> id_to_name = 1; // key:int32,value:string
map<string, bytes> file_content = 2; // 存储二进制文件
}
Oneof 互斥字段(节省传输体积)
oneof 表示互斥字段 :同一时间仅一个字段生效 ,自动清空其他字段,是 C++ 网络开发中节省传输流量的核心特性。
语法
oneof 名称 {
字段1 = 编号;
字段2 = 编号;
...
}
示例(网络消息通用)
message Message {
oneof msg_data {
string text = 1; // 文本消息
bytes image = 2; // 图片消息
int32 voice = 3; // 语音消息
}
}
C++ 开发用法 :生成的类提供
msg_data_case()方法,可直接判断当前生效的字段。
Package 与 Import(模块化开发)
Package(包)
-
对应 C++ 命名空间,解决多 proto 文件的命名冲突;
-
格式:
package 层级.层级;package com.company.network; // C++ 生成:com::company::network 命名空间
Import(导入)
-
普通导入:
import "xxx.proto";(仅当前文件使用); -
公共导入:
import public "xxx.proto";(传递依赖,子文件可复用)。import "google/protobuf/timestamp.proto";
import public "user.proto"; // 其他导入当前文件的 proto 可直接使用 User
Reserved 保留字段(协议兼容核心)
协议升级时,禁止重用旧字段的编号 / 名称 ,否则会导致数据解析异常。reserved 用于锁定废弃字段,是 C++ 分布式系统向后兼容的标准实践。
语法
message User {
reserved 2, 3, 5 to 10; // 保留字段编号
reserved "old_name", "old_age"; // 保留字段名称
int32 id = 1;
string new_name = 4;
}
选项(Option):C++ 性能优化核心
选项用于控制 proto 编译行为,C++ 网络开发重点关注以下选项:
全局文件级选项
// 开启 C++ Arena 内存池(**性能提升 30%+**,高频序列化必备)
option cc_enable_arenas = true;
// 优化模式:SPEED(默认,速度优先)/ CODE_SIZE(体积优先)/ LITE_RUNTIME(轻量版)
option optimize_for = SPEED;
// 生成 C++ 代码的命名空间(覆盖 package)
option cc_package = "MyProto";
字段级选项
message User {
// 强制关闭 packed 编码(极少用,proto3 默认 true)
repeated int32 ids = 1 [packed = false];
}
Service 服务定义(gRPC 核心)
proto3 原生支持服务定义,配合 gRPC 生成 C++ 服务端 / 客户端代码,是微服务通信的标准接口定义方式。
四种 RPC 调用模式
service UserService {
// 1. 简单 RPC:请求-响应
rpc GetUser(UserRequest) returns (UserResponse);
// 2. 服务端流:客户端发1次,服务端返多次
rpc ListUsers(UserRequest) returns (stream UserResponse);
// 3. 客户端流:客户端发多次,服务端返1次
rpc UploadUsers(stream UserRequest) returns (UserResponse);
// 4. 双向流:双发流式通信
rpc Chat(stream UserRequest) returns (stream UserResponse);
}
默认值规则(proto3 关键特性)
proto3 无显式默认值 ,所有字段默认是零值 ,且零值字段不会被序列化(节省传输体积):
- 数字类型:
0 - 布尔类型:
false - 字符串:
"" - 字节流:
"" - 枚举:第一个定义的 0 值
- 消息:
nullptr
C++ 开发避坑 :用
has_xxx()方法判断字段是否显式设置,而非判断值是否为零。
Protobuf3 语法禁忌(C++ 开发必记)
- 字段编号唯一,禁止重复;
oneof字段不能用required/repeated;map不能嵌套repeated;- 枚举必须包含 0 值;
- 协议升级必须用 reserved 锁定废弃字段;
- 高频字段优先使用 1~15 编号。
Protobuf3编译指令
C++ 核心基础编译指令(必掌握)
Protobuf3 为 C++ 生成代码的标准基础指令,是所有场景的根基:
bash
运行
protoc --cpp_out=<输出目录> <源.proto文件路径>
核心参数专业解释
--cpp_out:C++ 语言专属输出参数(Protobuf3 支持多语言,C++ 开发固定使用此参数),用于指定生成的 C++ 代码存储路径;- 输出目录 :支持相对路径 (
./)、绝对路径 (/home/dev/proto),目录不存在时protoc会自动创建; - 源 proto 文件 :待编译的协议定义文件(如
msg.proto)。
基础示例
当前目录下有user.proto,编译生成 C++ 代码到当前目录:
protoc --cpp_out=./ user.proto
执行后生成两个核心文件:user.pb.h(头文件)、user.pb.cc(源文件)。
高频进阶参数(C++ 大型网络工程必备)
C++ 网络开发中,多 proto 文件、import 依赖、多级目录模块化是常态,以下参数是工程化核心:
--proto_path / -I:指定协议搜索路径(解决 import 报错)
当 proto 文件中使用import "base.proto"导入依赖协议时,必须用此参数告诉protoc去哪里查找依赖文件。 标准语法:
protoc -I=<proto根目录> --cpp_out=<输出目录> <源proto文件>
-I是--proto_path的简写,支持指定多个搜索路径 (多次添加-I);- 这是 C++ 微服务、网络框架中必用参数 ,无此参数必然触发
import not found错误。
批量编译所有 proto 文件
C++ 网络工程通常包含数十个协议文件,批量编译指令:
protoc --cpp_out=./ ./*.proto
编译当前目录下所有.proto文件,一键生成对应 C++ 代码。
简单示例
contacts.proto
bash
// 首行:语法指定行
syntax = "proto3";
package contacts;
// 定义联系人message
message PeopleInfo {
//字段编号不能相同!!!
string name = 1; // 姓名
int32 age = 2; // 年龄
}
main.cpp
cpp
#include <iostream>
#include "contacts.pb.h"
int main()
{
std::string people_str;
{
// 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
contacts::PeopleInfo people;
people.set_name("张珊");
people.set_age(20);
if (!people.SerializeToString(&people_str)) {
std::cerr << "序列化联系⼈失败!" << std::endl;
return -1;
}
std::cout << "序列化成功,结果:" << people_str << std::endl;
}
{
// 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
contacts::PeopleInfo people;
if (!people.ParseFromString(people_str)) {
std::cerr << "反序列化联系⼈失败!" << std::endl;
return -1;
}
std::cout << "反序列化成功!" << std::endl
<< "姓名: " << people.name() << std::endl
<< "年龄: " << people.age() << std::endl;
}
return 0;
}
先编译.proto文件

就会生成对应的.h和.cpp文件

随后正常的编译输出即可

也可以这样
bash
# 编译器和标志
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -Wpedantic -O2
PROTOBUF_CXXFLAGS = $(shell pkg-config --cflags protobuf)
PROTOBUF_LDFLAGS = $(shell pkg-config --libs protobuf)
# 源文件和目标文件
SRCS = main.cpp contacts.pb.cc
TARGET = main
.PHONY: all clean proto
# 默认编译所有目标
all: proto $(TARGET)
# 生成 protobuf 文件
proto:
protoc --cpp_out=. contacts.proto
# 链接可执行文件
$(TARGET): $(SRCS:.cpp=.o)
$(CXX) $^ -o $@ $(PROTOBUF_LDFLAGS)
# 编译规则
%.o: %.cpp
$(CXX) $(CXXFLAGS) $(PROTOBUF_CXXFLAGS) -c $< -o $@
# 清理生成的文件
clean:
rm -f $(TARGET) *.o contacts.pb.cc contacts.pb.h
本期内容就到这里了,喜欢请点个赞谢谢
封面图自取:
