批量上传历史数据
场景说明
在嵌入式和物联网开发中,经常需要批量上传历史数据,比如温度传感器的采集记录。Protobuf 提供了高效的序列化方式,特别适合这种场景。
1. 定义 .proto 文件
我们先定义一个 history.proto 文件,描述批量上传的数据结构:
protobuf
syntax = "proto3";
package storage;
message Record {
int64 timestamp = 1;
float temperature = 2;
}
message BatchUpload {
int32 batch_id = 1;
repeated Record records = 2; // 多个记录
repeated string tags = 3; // 标签
}
2. C++ 示例代码
下面是对应的 C++ 代码,演示 repeated 字段的高效操作:
cpp
#include <iostream>
#include "history.pb.h"
int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
// 1. 创建 BatchUpload 对象
storage::BatchUpload batch;
batch.set_batch_id(20251206);
// 2. 添加 Record 对象(推荐方式)
for (int i = 0; i < 3; ++i) {
auto* rec = batch.add_records();
rec->set_timestamp(1000 + i);
rec->set_temperature(20.0 + i);
}
// 3. 添加标签
batch.add_tags("urgent");
batch.add_tags("sensor_a");
// 4. 修改第2个记录(索引1)
auto* rec2 = batch.mutable_records(1);
rec2->set_temperature(99.9);
// 5. 打印数组长度
std::cout << "记录数量: " << batch.records_size() << std::endl;
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
3. 编译与运行
bash
protoc --cpp_out=. history.proto
g++ main.cpp history.pb.cc -lprotobuf -o main
./main
4. 学
- 链式添加 :
add_records()返回新对象指针,可以一行完成对象创建和赋值。 - 修改特定项 :用
mutable_records(index)获取指针,直接修改数组中的某一项。 - 高效操作 repeated 字段:避免不必要的拷贝和低效写法,提升性能。
- Protobuf 的 C++ API 设计:与 STL 容器类似,易于上手。
使用 Protobuf 的 Timestamp 与 TimeUtil 在 C++ 中优雅处理时间
- 背景说明 :
在很多老代码里,时间戳常常被用int64(秒或毫秒)来存储和传递。这种做法会带来可读性差、时区/纳秒处理复杂、JSON/日志中显示为"大整数"不友好等问题。Google Protobuf 自带的google.protobuf.Timestamp是对时间的标准表示(可精确到纳秒),配合google::protobuf::util::TimeUtil,能让时间处理更安全、语义更明确、对外表达更友好。
我们要学的内容
- 目标 : 使用 Google 官方提供的
google.protobuf.Timestamp替代int64时间戳。 - 关键点 : 不要手动用
time(NULL)再set_seconds(),而是用TimeUtil::GetCurrentTime()获取时间并赋值;用TimeUtil::ToString()或 JSON 工具输出可读字符串。
示例 .proto 文件
- 文件: logger.proto
- 内容示例:
proto
syntax = "proto3";
package sys;
import "google/protobuf/timestamp.proto";
message EventLog {
string content = 1;
google.protobuf.Timestamp created_at = 2;
}
为什么这样做
- 可读性 : 在 JSON 或日志中,
Timestamp会以 ISO 8601 风格字符串输出(例如"2023-12-01T12:00:00Z"),比大整数更直观。 - 精度: 支持纳秒级别,不再丢失子秒信息。
- 互操作 : Protobuf 工具链(JSON 转换、语言绑定)会正确理解
Timestamp类型,避免自定义协议歧义。
**C++ 示例
- 文件: main.cpp
cpp
// TODO:
// 1. Include header: <google/protobuf/util/time_util.h> (Crucial for handling time).
//
// 2. Create a "sys::EventLog" object.
// - Set content to "System Booted".
//
// 3. Set "created_at" to Current Time:
// - Do NOT manually set seconds.
// - Use "google::protobuf::util::TimeUtil::GetCurrentTime()" to get a Timestamp object.
// - Assign it using "log.mutable_created_at()->CopyFrom(...)".
// - OR simply: *log.mutable_created_at() = TimeUtil::GetCurrentTime();
//
// 4. Convert to String for printing:
// - Use "TimeUtil::ToString(log.created_at())" to print a human-readable ISO string.
//
// 核心提示: 如果你手动去 set_seconds(time(NULL)),虽然也能跑,但这太"土"了。
#include <iostream>
#include <string>
#include "logger.pb.h"
#include <google/protobuf/util/time_util.h>
#include <google/protobuf/stubs/common.h>
int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
sys::EventLog log;
log.set_content("System Booted");
// 使用 TimeUtil 获取当前时间并赋值
using google::protobuf::util::TimeUtil;
*log.mutable_created_at() = TimeUtil::GetCurrentTime();
// 将 Timestamp 转为可读的 ISO 字符串
std::string created_at_str = TimeUtil::ToString(log.created_at());
std::cout << "content: " << log.content() << std::endl;
std::cout << "created_at: " << created_at_str << std::endl;
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
如何把消息转成 JSON(展示 Timestamp 在 JSON 中的可读性)
- 在 C++ 中可以使用
MessageToJsonString(需要google/protobuf/util/json_util.h):
cpp
#include <google/protobuf/util/json_util.h>
// ... 假设有 sys::EventLog log 已赋值
std::string json_str;
google::protobuf::util::MessageToJsonString(log, &json_str);
std::cout << json_str << std::endl;
- 输出示例(
created_at会是 ISO 字符串):
json
{
"content": "System Booted",
"createdAt": "2025-12-06T16:48:31Z"
}
(注意:字段命名风格可能根据 JSON 转换选项有所变化。)
常见误区
- 误区 : "把时间作为
int64更简单、占空间少"。
纠正 : 虽然int64可行,但会让跨语言/跨系统表达变得脆弱。使用Timestamp可让 Protobuf 工具链自动处理格式、精度和序列化细节。 - 误区 : "自己调用
time(NULL)并手动 set 秒数就够了"。
纠正 : 这样会丢失纳秒信息且容易写错。TimeUtil::GetCurrentTime()是官方推荐做法。
cpp
#include <iostream>
#include <fstream>
#include <string>
#include "logger.pb.h"
#include <google/protobuf/util/time_util.h>
#include <google/protobuf/util/json_util.h>
#include <google/protobuf/stubs/common.h>
int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
sys::EventLog log;
log.set_content("System Booted");
using google::protobuf::util::TimeUtil;
*log.mutable_created_at() = TimeUtil::GetCurrentTime();
// Serialize to binary file
const char* filename = "eventlog.bin";
std::ofstream ofs(filename, std::ios::binary);
if (!ofs) {
std::cerr << "Failed to open " << filename << " for writing\n";
return 2;
}
if (!log.SerializeToOstream(&ofs)) {
std::cerr << "Failed to serialize log to file\n";
return 3;
}
ofs.close();
std::cout << "Serialized to " << filename << "\n";
// Read it back
sys::EventLog log2;
std::ifstream ifs(filename, std::ios::binary);
if (!ifs) {
std::cerr << "Failed to open " << filename << " for reading\n";
return 4;
}
if (!log2.ParseFromIstream(&ifs)) {
std::cerr << "Failed to parse log from file\n";
return 5;
}
ifs.close();
// Print human-readable time via TimeUtil
std::string created_at_str = TimeUtil::ToString(log2.created_at());
std::cout << "read back content: " << log2.content() << "\n";
std::cout << "read back created_at: " << created_at_str << "\n";
// Print JSON
std::string json;
google::protobuf::util::MessageToJsonString(log2, &json);
std::cout << "JSON:\n" << json << "\n";
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
Arena Allocation(竞技场分配)在 C++ + Protobuf 中的实战指南
简介
在高性能系统或嵌入式场景下,频繁的 new / delete 会带来内存碎片和性能波动。Google Protobuf 提供的 Arena(竞技场分配器)可以显著改善分配速度并实现批量释放------非常适合短生命周期、高频率创建的 Message 对象。
本次学习目标
- 理解 Arena 的两个核心优势:
- 极速分配(在一大块连续内存上只移动指针)。
- 批量释放(销毁 Arena 即释放其上分配的所有对象)。
- 学会在 C++ 中将 Protobuf Message 分配到
google::protobuf::Arena,并注意生命周期与禁止delete的规则。
示例说明(项目文件)
以下示例已经放在你的工作区:
perf.proto:定义fast.DataPacket。arena_demo.cpp:在Arena上创建fast::DataPacket、设置字段、展示输出并说明生命周期。
perf.proto 内容
proto
syntax = "proto3";
package fast;
message DataPacket {
int32 id = 1;
string payload = 2;
repeated int32 numbers = 3;
}
关键 C++ 示例(摘录自 arena_demo.cpp)
cpp
#include "perf.pb.h"
#include <google/protobuf/arena.h>
int main() {
// 在栈上创建 Arena
google::protobuf::Arena arena;
// 在 Arena 上创建 Message(快速分配)
fast::DataPacket* pkt = google::protobuf::Arena::CreateMessage<fast::DataPacket>(&arena);
// 正常使用对象
pkt->set_id(1024);
pkt->set_payload("Arena is fast");
for (int i = 0; i < 5; ++i) pkt->add_numbers(i * 10);
// 不要 delete pkt!当 arena 离开作用域时,所有在其上分配的对象会被统一回收
}
示例输出(你在本地运行得到的)
pkt.id = 1024
pkt.payload = Arena is fast
pkt.numbers: 0 10 20 30 40
Exiting function, arena will clean up everything...
为什么使用 Arena(深入理解)
- 分配速度:Arena 在内部管理一块或多块大内存,分配仅是指针向前推进,远快于
malloc/new。这对短生命周期对象尤为重要。 - 内存碎片:减少了频繁小对象分散在堆上的情况,降低碎片化风险。
- 批量释放:一次性销毁 Arena 即释放上面所有对象,代码更简洁,也减少内存泄漏的可能。
注意事项与最佳实践
- 绝对不要手动
delete在 Arena 上创建的对象(双重释放会导致崩溃)。 - Arena 适合短生命周期对象或"分配后不再 individually delete"的场景。对于需要单独控制生命周期的对象,仍需使用常规堆分配。
- 如果使用 arena 分配大量动态字符串或容器,关注内存总消耗并酌情调整 arena 的初始/扩展策略(Protobuf Arena 会自动扩展,但仍需监控)。
- 在多线程环境中,通常每个线程维护自己的 Arena 可以避免锁竞争(根据业务场景设计)。
Protobuf 反射(Reflection)实战指南 --- 在 C++ 中动态读写消息字段
简介
当你的程序需要做通用协议处理、网关、配置管理器或调试工具时,你往往并不知道具体的消息类型或字段名,只有字符串形式的字段名(如 "port")。Protobuf 自带的 Descriptor + Reflection 可以让你在运行时动态地枚举、读取与写入消息字段------这就是 Protobuf 的反射机制。
本次学习目标
- 理解
Descriptor(元数据)和Reflection(读写接口)的作用与区别。 - 在 C++ 中用
GetDescriptor()/GetReflection()实现按字段名动态读写与遍历。 - 学会在工具或网关中通过反射实现通用的 Protobuf 处理逻辑(例如 MessageToJsonString 的工作原理)。
.proto 示例(dynamic.proto)
示例文件 dynamic.proto:
proto
syntax = "proto3";
package dyn;
message DeviceConfig {
string server_ip = 1;
int32 port = 2;
bool is_debug = 3;
}
核心 C++ 示例(摘自 reflection_demo.cpp)
下面示例展示了:初始化消息、通过 GetDescriptor() 获取元数据、通过 GetReflection() 动态读写字段,以及遍历所有字段并打印它们的值。
cpp
#include "dynamic.pb.h"
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
int main() {
dyn::DeviceConfig config;
config.set_server_ip("192.168.1.1");
config.set_port(8080);
config.set_is_debug(true);
// Descriptor(元数据)
const google::protobuf::Descriptor* descriptor = config.GetDescriptor();
// Reflection(读写接口)
const google::protobuf::Reflection* reflection = config.GetReflection();
// 按字段名动态读取
std::string field_name = "port";
const google::protobuf::FieldDescriptor* field = descriptor->FindFieldByName(field_name);
if (field && field->type() == google::protobuf::FieldDescriptor::TYPE_INT32) {
int value = reflection->GetInt32(config, field);
std::cout << "Dynamic read: " << field_name << " = " << value << std::endl;
}
// 动态写入
auto fd = descriptor->FindFieldByName("server_ip");
if (fd && fd->type() == google::protobuf::FieldDescriptor::TYPE_STRING) {
reflection->SetString(&config, fd, "10.0.0.5");
}
// 遍历字段并打印
for (int i = 0; i < descriptor->field_count(); ++i) {
const google::protobuf::FieldDescriptor* f = descriptor->field(i);
std::cout << f->name() << "(" << f->number() << ") type=" << f->type_name();
// 用 Reflection 取值(示例只处理 string/int32/bool)
if (f->type() == google::protobuf::FieldDescriptor::TYPE_STRING) {
std::cout << ", value=" << reflection->GetString(config, f);
} else if (f->type() == google::protobuf::FieldDescriptor::TYPE_INT32) {
std::cout << ", value=" << reflection->GetInt32(config, f);
} else if (f->type() == google::protobuf::FieldDescriptor::TYPE_BOOL) {
std::cout << ", value=" << (reflection->GetBool(config, f) ? "true" : "false");
}
std::cout << std::endl;
}
}
示例运行输出(本地实测)
Dynamic read: port = 8080
Dynamic write: server_ip = 10.0.0.5
Iterate fields:
- name: server_ip, number: 1, type: string, value: 10.0.0.5
- name: port, number: 2, type: int32, value: 8080
- name: is_debug, number: 3, type: bool, value: true
原理提示(MessageToJsonString)
MessageToJsonString 的底层其实就是遍历 Descriptor,然后用 Reflection 读取每个字段并把值格式化成 JSON 字符串。掌握 Descriptor + Reflection,就能写出自定义的序列化器、通用验证器、配置迁移工具或网关路由逻辑。
实战建议与注意事项
- 反射是强大的,但比直接访问字段要慢(因为要做查找、类型判断和分支)。在性能敏感的路径上,尽量用静态绑定。把反射用于通用工具、管理/调试/网关层。
FindFieldByName返回nullptr时要小心处理(字段不存在或拼写错误)。- 对于重复字段或嵌套消息,需要使用 Reflection 提供的
AddMessage/MutableMessage/GetRepeatedField等 API,注意类型匹配。 - 如果需要高性能的通用 JSON 输出,考虑缓存
Descriptor对象里常用字段的FieldDescriptor*指针,以避免频繁FindFieldByName。