序列化与反序列化及其ProtoBuf学习总结

前面我们已经在Linux网络中学习了序列化相关的内容,并且利用JsonCpp三方库进行序列化和反序列化。本期我们就来介绍另一个适用于序列化的库ProtoBuf

相关代码以及提交到作者的个人gitee:ProtoBuf代码: 本代码仅仅只是我用来学习ProtoBuf第三方库的代码仓库

目录

序列化与反序列化

ProtoBuf的特点

契约驱动的代码生成机制

极致的空间效率

卓越的时间效率

下载ProtoBuf

验证安装

Windows

Ubuntu

Centos

ProtoBuf3语法

[核心基础:.proto 文件标准结构](#核心基础:.proto 文件标准结构)

[标量数据类型(proto3 ↔ C++ 类型映射)](#标量数据类型(proto3 ↔ C++ 类型映射))

[字段规则(proto3 核心:取消 required)](#字段规则(proto3 核心:取消 required))

枚举类型(enum)

消息类型(message)

基础消息

嵌套消息

导入外部消息

[Map 类型(原生键值对)](#Map 类型(原生键值对))

语法

规则

示例

[Oneof 互斥字段(节省传输体积)](#Oneof 互斥字段(节省传输体积))

语法

示例(网络消息通用)

[Package 与 Import(模块化开发)](#Package 与 Import(模块化开发))

Package(包)

Import(导入)

[Reserved 保留字段(协议兼容核心)](#Reserved 保留字段(协议兼容核心))

语法

[选项(Option):C++ 性能优化核心](#选项(Option):C++ 性能优化核心)

全局文件级选项

字段级选项

[Service 服务定义(gRPC 核心)](#Service 服务定义(gRPC 核心))

[四种 RPC 调用模式](#四种 RPC 调用模式)

[默认值规则(proto3 关键特性)](#默认值规则(proto3 关键特性))

[Protobuf3 语法禁忌(C++ 开发必记)](#Protobuf3 语法禁忌(C++ 开发必记))

Protobuf3编译指令

[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下最直接的方式是下载预编译二进制包:

  1. 访问GitHub Releases页面,选择适合的版本(如protoc-21.11-win64.zip

  2. 将压缩包解压到指定目录,例如D:\protobuf

  3. 随后在环境变量中系统变量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++ 开发关键建议

  1. 传输负数必须用 sint32/sint64,varint 编码效率提升 50%+;
  2. 大数值(>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++ 枚举类,是网络协议中状态码、类型定义的标准方案。

核心语法规则

  1. 必须包含 0 值枚举(proto3 强制),作为默认值;
  2. 枚举值必须是正整数
  3. 支持嵌套枚举;
  4. 允许别名(需开启 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 = 编号;

规则

  1. key 仅支持:标量数字 / 字符串(禁止枚举、消息);
  2. value 支持:任意类型(除 map);
  3. 不支持 repeated 嵌套 map;
  4. 序列化后为有序键值对,但解析后无序。

示例

复制代码
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++ 开发必记)

  1. 字段编号唯一,禁止重复;
  2. oneof 字段不能用 required/repeated
  3. map 不能嵌套 repeated
  4. 枚举必须包含 0 值;
  5. 协议升级必须用 reserved 锁定废弃字段;
  6. 高频字段优先使用 1~15 编号。

Protobuf3编译指令

C++ 核心基础编译指令(必掌握)

Protobuf3 为 C++ 生成代码的标准基础指令,是所有场景的根基:

bash

运行

复制代码
protoc --cpp_out=<输出目录> <源.proto文件路径>

核心参数专业解释

  1. --cpp_outC++ 语言专属输出参数(Protobuf3 支持多语言,C++ 开发固定使用此参数),用于指定生成的 C++ 代码存储路径;
  2. 输出目录 :支持相对路径./)、绝对路径/home/dev/proto),目录不存在时protoc会自动创建;
  3. 源 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

本期内容就到这里了,喜欢请点个赞谢谢

封面图自取:

相关推荐
3秒一个大2 小时前
深入理解 Node.js:生态体系与事件循环机制详解
前端·后端·node.js
Gkoob2 小时前
Vue3+Three.js 打造实时设备状态 3D 可视化面板
开发语言·javascript·3d
m0_716765232 小时前
C++巩固案例--通讯录管理系统详解
java·开发语言·c++·经验分享·学习·青少年编程·visual studio
喵个咪2 小时前
Apache Doris 4.x 在量化交易中的完整应用实践
后端·架构·ai编程
Uncertainty!!2 小时前
无法打开校园网认证网页问题
网络
G果2 小时前
ros2工程 debug(vscode)
c++·ide·vscode·编辑器·bug·debug·ros2
当时只道寻常2 小时前
NestJS Redis 原子限流守卫 防刷防攻击
后端·nestjs
jf加菲猫2 小时前
第10章 数据处理
xml·开发语言·数据库·c++·qt·ui
笔夏2 小时前
【安卓学习之socket】socket.io-client
android·学习
酉鬼女又兒2 小时前
零基础快速入门前端深入掌握箭头函数、Promise 与 Fetch API —— 蓝桥杯 Web 考点全解析(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·css·职场和发展·蓝桥杯·es6·js