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
相关推荐
HLC++14 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室15 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy17 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕18 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫19 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生20 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1121 小时前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器