C++:Protobuf 动态加载 .pb 文件并发送 Message(附带源码)
一、项目背景详细介绍
在传统的 C++ + Protobuf 使用方式中,开发流程通常是:
-
编写
.proto文件 -
使用
protoc编译生成.pb.h / .pb.cc -
在项目中 静态链接 使用生成的 Message 类型
这种方式在大多数业务中是完全可行的,但在以下场景中会遇到明显瓶颈:
-
协议需要动态扩展
-
服务端无法提前知道所有消息类型
-
插件化 / 脚本化系统
-
通用 RPC 网关 / 协议转发器
-
中间件、调试工具、抓包重放系统
例如:
-
一个通用消息网关,需要转发任意 Protobuf 消息
-
一个服务器运行中热更新协议
-
一个测试工具,运行时加载
.proto文件并构造消息
此时,静态编译 .pb.cc 的方式已经不适用。
Protobuf 官方为此提供了一整套 运行时反射(Reflection)与动态描述机制,允许我们:
在不生成 .pb.cc 的情况下,动态加载 .proto 文件,并创建、填充、序列化 Message
本项目的目标是:
使用 C++ 实现 Protobuf
.proto文件的动态加载,并构造 / 发送 Message 数据
二、项目需求详细介绍
2.1 功能需求
-
运行时加载
.proto文件 -
不依赖静态生成的
.pb.h / .pb.cc -
使用 Protobuf 反射机制:
-
创建 Message 实例
-
设置字段值
-
-
将 Message:
-
序列化
-
发送(以模拟发送为例)
-
2.2 技术要求
-
编程语言:C++
-
Protobuf 版本:proto2 / proto3 均适用
-
使用模块:
-
google::protobuf::compiler -
google::protobuf::Descriptor -
google::protobuf::DynamicMessageFactory
-
-
所有类型解析发生在 运行时
2.3 设计要求
-
面向教学 / 博客输出
-
文章 ≥ 5000 中文字符
-
严格包含以下结构:
-
项目背景详细介绍
-
项目需求详细介绍
-
相关技术详细介绍
-
实现思路详细介绍
-
完整实现代码(单一代码块)
-
代码详细解读(只讲方法作用)
-
项目详细总结
-
项目常见问题及解答
-
扩展方向与性能优化
-
三、相关技术详细介绍
3.1 Protobuf 的两种使用模式
1️⃣ 静态模式(最常见)
-
.proto→protoc→.pb.cc -
编译期类型确定
-
性能最好
-
不灵活
2️⃣ 动态模式(高级用法)
-
.proto在 运行时解析 -
使用 Descriptor + Reflection
-
不生成
.pb.cc -
高度灵活
3.2 Protobuf 核心动态组件
Descriptor(描述符)
-
描述:
-
Message
-
Field
-
Enum
-
-
相当于 Protobuf 的“类型元数据”
Reflection(反射)
-
允许在不知道具体类型的情况下:
-
读取字段
-
写入字段
-
遍历字段
-
DynamicMessageFactory
-
根据 Descriptor:
-
动态创建 Message 实例
-
3.3 动态加载 .proto 的核心类
| 类 | 作用 |
|---|---|
| DiskSourceTree | 管理 proto 文件路径 |
| Importer | 解析 proto 文件 |
| DescriptorPool | 存储解析后的描述信息 |
| DynamicMessageFactory | 创建 Message |
3.4 动态 Message 的典型应用
-
RPC 网关
-
协议适配器
-
Protobuf 调试工具
-
插件化服务
-
通用转发 / 存储系统
四、实现思路详细介绍
4.1 整体流程设计
完整流程如下:
-
指定
.proto搜索路径 -
使用
Importer加载.proto -
从
DescriptorPool获取 MessageDescriptor -
使用
DynamicMessageFactory创建 Message -
通过 Reflection 设置字段值
-
序列化 Message
-
发送(本例中打印 / 模拟发送)
4.2 示例 .proto 结构(逻辑说明)
假设我们有如下 proto(无需生成 pb.cc):
syntax = "proto3"; message Person { string name = 1; int32 age = 2; }
4.3 字段设置策略
-
使用
FieldDescriptor -
根据字段类型调用:
-
SetString -
SetInt32 -
SetXXX
-
4.4 发送模型说明
-
本示例中:
-
使用
SerializeToString -
模拟网络发送(打印十六进制)
-
五、完整实现代码
/****************************************************
* 文件名:DynamicProtobuf.cpp
* 描述:C++ 动态加载 Protobuf .proto 并发送 Message
****************************************************/
#include <iostream>
#include <memory>
#include <fstream>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/message.h>
#include <google/protobuf/descriptor.h>
using namespace std;
using namespace google::protobuf;
using namespace google::protobuf::compiler;
/****************************************************
* 自定义错误输出
****************************************************/
class ProtoErrorCollector : public MultiFileErrorCollector
{
public:
void AddError(const string& filename, int line, int column,
const string& message) override
{
cerr << filename << ":" << line << ":" << column
<< " " << message << endl;
}
};
/****************************************************
* 主函数
****************************************************/
int main()
{
// 1. 设置 proto 搜索路径
DiskSourceTree sourceTree;
sourceTree.MapPath("", "./proto"); // proto 文件所在目录
// 2. 创建 Importer
ProtoErrorCollector errorCollector;
Importer importer(&sourceTree, &errorCollector);
// 3. 动态加载 proto 文件
const FileDescriptor* fileDesc =
importer.Import("person.proto");
if (!fileDesc)
{
cerr << "加载 proto 文件失败" << endl;
return -1;
}
// 4. 获取 Message 描述符
const Descriptor* msgDesc =
fileDesc->FindMessageTypeByName("Person");
if (!msgDesc)
{
cerr << "未找到 Person Message" << endl;
return -1;
}
// 5. 创建动态 Message
DynamicMessageFactory factory;
const Message* prototype =
factory.GetPrototype(msgDesc);
unique_ptr<Message> message(prototype->New());
// 6. 使用反射设置字段
const Reflection* reflection = message->GetReflection();
const FieldDescriptor* nameField =
msgDesc->FindFieldByName("name");
const FieldDescriptor* ageField =
msgDesc->FindFieldByName("age");
reflection->SetString(message.get(), nameField, "Alice");
reflection->SetInt32(message.get(), ageField, 18);
// 7. 序列化 Message
string output;
message->SerializeToString(&output);
cout << "序列化后字节长度:" << output.size() << endl;
cout << "模拟发送数据(hex):" << endl;
for (unsigned char c : output)
{
printf("%02X ", c);
}
cout << endl;
return 0;
}
六、代码详细解读(仅解读方法作用)
-
DiskSourceTree:配置.proto文件搜索路径 -
Importer:运行时解析.proto文件 -
FileDescriptor:描述整个 proto 文件 -
Descriptor:描述单个 Message 类型 -
DynamicMessageFactory:创建动态 Message 实例 -
Reflection:用于字段读写 -
SerializeToString:将 Message 序列化为二进制数据
七、项目详细总结
通过本项目,你已经系统掌握:
-
Protobuf 的 动态加载机制
-
Descriptor / Reflection 的真实作用
-
不依赖
.pb.cc的 Message 构造方式 -
运行时协议解析的完整流程
-
高级中间件 / 网关的核心技术基础
这是从:
“会用 Protobuf” → “理解 Protobuf 内核机制”
的关键跨越项目。
八、项目常见问题及解答
Q1:动态方式性能如何?
A:低于静态方式,但在网关 / 控制面场景完全可接受。
Q2:能否动态解析 proto3?
A:可以,proto2 / proto3 机制一致。
Q3:可以反序列化吗?
A:完全可以,使用 ParseFromString。
九、扩展方向与性能优化
-
支持 任意 Message 动态转发
-
与 socket / gRPC 结合
-
DescriptorPool 复用优化
-
支持 JSON ↔ Protobuf 转换
-
构建通用 Protobuf 调试工具
更多推荐




所有评论(0)