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

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

目录

序列化与反序列化

ProtoBuf的特点

        契约驱动的代码生成机制

        极致的空间效率

        卓越的时间效率

下载ProtoBuf

        验证安装

        Windows

        Ubuntu

        Centos

ProtoBuf3语法

        核心基础:.proto 文件标准结构

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

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

        枚举类型(enum)

        消息类型(message)

        基础消息

        嵌套消息

        导入外部消息

        Map 类型(原生键值对)

        语法

        规则

        示例

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

语法

示例(网络消息通用)

        Package 与 Import(模块化开发)

        Package(包)

        Import(导入)

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

语法

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

        全局文件级选项

        字段级选项

        Service 服务定义(gRPC 核心)

        四种 RPC 调用模式

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

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

Protobuf3编译指令

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

核心参数专业解释

基础示例

        高频进阶参数(C++ 大型网络工程必备)

        --proto_path / -I:指定协议搜索路径(解决 import 报错)

        批量编译所有 proto 文件

简单示例


序列化与反序列化

        在单机程序中,数据以内存对象的形式存在——结构体、类实例、容器等。这些对象依赖于特定的编译器实现、字节对齐方式、甚至CPU架构(大小端)。当我们需要将这些数据跨进程(不同地址空间)、跨网络(不同机器)、跨语言(不同运行时)或者跨时间(存储后将来恢复)进行交换时,就必须将内存对象转换为一种与具体环境解耦的、可传输的字节序列。这个转换过程就是序列化,其逆过程称为反序列化

        总而言是:

        序列化:把对象转换为字节序列的过程 称为对象的序列化。 
        反序列化:把字节序列恢复为对象的过程 称为对象的反序列化

        序列化方案本质上是在解决三个核心矛盾:

  • 空间效率:序列化后的数据越小,网络带宽和存储成本越低。

  • 时间效率:序列化/反序列化过程消耗的CPU时间越少,系统吞吐量越高。

  • 可理解性与可演进性:数据是否便于人类排查问题?协议升级时,新旧版本能否共存?

        目前并没有完美解决这三点的方案,主流的序列化工具有Json、ProtoBuf、XML等。

ProtoBuf的特点

        ProtoBuf(Protocol Buffers)是Google设计的一种语言中立、平台无关、可扩展的二进制序列化协议,同时配套了完整的代码生成工具链。它在高性能分布式系统中被广泛采用

        契约驱动的代码生成机制

        ProtoBuf最显著的特点之一就是强制先定义数据契约。你需要用.proto文件声明消息结构

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()判断是否存在

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显示已注册的protobuf库

        Windows

        官方网站:Releases · protocolbuffers/protobuf

        Windows下最直接的方式是下载预编译二进制包:

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

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

  3. 随后在环境变量中系统变量Path中新增一条记录,填入D:\protobuf\bin(以实际下载安装路径为准)

        Ubuntu

sudo apt-get update
sudo apt-get install -y protobuf-compiler libprotobuf-dev

        其中protobuf-compiler是编译器,libprotobuf-dev是C++开发库

        Centos

        Centos的ProtoBuf的版本相对较老

sudo yum install -y protobuf-compiler protobuf-devel

ProtoBuf3语法

        核心基础:.proto 文件标准结构

        proto3 的协议定义文件以 .proto 为后缀,第一行必须声明语法版本,这是强制要求。

        我们可以参考以下的格式

// 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++ 对应类型编码方式说明
doubledoublefixed6464 位浮点数
floatfloatfixed3232 位浮点数
int32int32_tvarint32 位有符号整数(负数效率低)
int64int64_tvarint64 位有符号整数(负数效率低)
uint32uint32_tvarint32 位无符号整数
uint64uint64_tvarint64 位无符号整数
sint32int32_tvarint32 位有符号整数(负数优先用
sint64int64_tvarint64 位有符号整数(负数优先用
fixed32uint32_tfixed3232 位定长无符号(数值 > 2²⁸用)
fixed64uint64_tfixed6464 位定长无符号(数值 > 2⁵⁶用)
sfixed32int32_tfixed3232 位定长有符号
sfixed64int64_tfixed6464 位定长有符号
boolboolvarint布尔值
stringstd::stringUTF-8字符串(长度不超过 2³²)
bytesstd::string原生字节二进制字节流(C++ 用 string 存储)

C++ 开发关键建议

  1. 传输负数必须用 sint32/sint64,varint 编码效率提升 50%+;
  2. 大数值(>2²⁸)用 fixed 类型,避免变长编码的性能损耗。

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

        proto3 彻底移除了 proto2 的 required 规则,仅保留两种字段规则,这是与 proto2 的最大区别:

        默认规则:singular(单字段)

  • 不写任何修饰符,默认规则

  • 一个消息中,该字段最多出现一次

  • 可选字段,不设置则使用默认值。

message User {
  int32 id = 1; // singular:单字段,默认规则
  string name = 2;
}

        repeated(重复字段)

  • 表示数组 / 列表,对应 C++ 的 std::vector

  • proto3 默认开启 packed 编码(二进制紧凑打包),性能远优于 proto2;

  • 支持任意标量 / 枚举 / 消息类型。

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 选项)。
// 全局枚举
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

// 首行:语法指定行
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo {
  //字段编号不能相同!!!
  string name = 1;  // 姓名
  int32 age = 2;    // 年龄  
}

        main.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文件

        

        随后正常的编译输出即可

        也可以这样

# 编译器和标志
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

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

封面图自取:

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐