protobuf[2]

批量上传历史数据

场景说明

在嵌入式和物联网开发中,经常需要批量上传历史数据,比如温度传感器的采集记录。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 的 TimestampTimeUtil 在 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 的两个核心优势:
    1. 极速分配(在一大块连续内存上只移动指针)。
    2. 批量释放(销毁 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
相关推荐
sky北城1 小时前
Linux的回收站机制实现方式总结
linux·运维·服务器
代码游侠1 小时前
复习——栈、队列、树、哈希表
linux·数据结构·学习·算法
橘子真甜~1 小时前
C/C++ Linux网络编程10 - http协议
linux·服务器·网络·c++·网络协议·http
zimoyin2 小时前
WSL音频转发配置流程:WSL2/WSL1全适配
linux·音视频·wsl·虚拟机·ekho
2401_853448232 小时前
busybox制作根文件系统
linux·busybox·系统移植
元亓亓亓2 小时前
LeetCode热题100--20. 有效的括号--简单
linux·算法·leetcode
实心儿儿2 小时前
Linux —— 基础开发工具4
linux·运维·服务器
diegoXie3 小时前
WSL2 跨系统文件移动
linux·ubuntu
一尘之中3 小时前
Linux命令行查看磁盘大小完全指南
linux·运维·ai写作