前置条件:

参考博客:

ProtoBuf 的定义和描述

Protocol Buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

怎么感觉到处都在对比 XML 的速度慢啊...

你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

使用 ProtoBuf教程:

对 ProtoBuf 的基本概念有了一定了解之后,我们来看看具体该如何使用 ProtoBuf。

第一步,创建 .proto 文件,定义数据结构,如下所示:

//person.proto
package test; message Person {
required string name = 1;
required int32 id = 2;
required string email=3;
}

ProtoBuf 数据类型

proto文件消息类型 C++ 类型 说明
double double 双精度浮点型
float float 单精度浮点型
int32 int32 使用可变长编码方式,负数时不够高效,应该使用sint32
int64 int64 同上
uint32 uint32 使用可变长编码方式
uint64 uint64 同上
sint32 int32 使用可变长编码方式,有符号的整型值,负数编码时比通常的int32高效
sint64 sint64 同上
fixed32 uint32 总是4个字节,如果数值总是比2^28大的话,这个类型会比uint32高效
fixed64 uint64 总是8个字节,如果数值总是比2^56大的话,这个类型会比uint64高效
sfixed32 int32 总是4个字节
sfixed64 int64 总是8个字节
bool bool
string string 一个字符串必须是utf-8编码或者7-bit的ascii编码的文本
bytes string 可能包含任意顺序的字节数据

我们在上例中定义了一个名为 Person的消息,语法很简单,message 关键字后跟上消息名称。之后我们在其中定义了 message 具有的字段,形式为:

message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
// 请注意,范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型。
// 范围 16 至 2047 中的字段编号需要两个字节。
// 所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。
// 请记住为将来可能添加的频繁出现的字段留出一些空间。
字段规则 类型 名称 = 字段编号;
}

第二步,protoc 编译 .proto 文件生成读写接口:

.proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。ProtoBuf 提供相应的接口代码,可以通过 protoc 这个编译器来生成相应的接口代码,命令如下:

// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码 protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto protoc ./person.proto --cpp_out=./

生成的.h,.cpp文件为person.pb.h,person.pb.cpp,且.h的定义与proto文件的内容相关联:

namespace test { // 对应 package test;

class Person : public ::google::protobuf::Message { //对应 message Person 且继承自::google::protobuf::Message
public: inline void set_name(const ::std::string& value); //对应message的字段内容
inline void set_email(const ::std::string& value);
inline void set_id(::google::protobuf::int32 value);
...
}

第三步,编写C++业务代码:

// Person_Test.cpp
#include <iostream>
#include <fstream>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
#include <fmt/core.h>
#include "person.pb.h" using namespace test; int main() {
// 构造 Person 并填充信息
Person p;
p.set_name("Koshkaaa");
p.set_id(100);
p.set_email("kannajiahe@gmail.com"); // 将 pb 二进制信息保存到字符串, 序列化
std::string str;
p.SerializeToString(&str);
fmt::print("str: [ {} ]\n", str); // 将 pb 文件信息写入文件
std::ofstream fw;
fw.open("../Person.txt", std::ios::out | std::ios::binary);
auto *out = new google::protobuf::io::OstreamOutputStream(&fw);
google::protobuf::TextFormat::Print(p, out); delete out;
fw.close(); // 将 pb 文本信息保存到字符串
std::string str1;
google::protobuf::TextFormat::PrintToString(p, &str1);
fmt::print("str1: [ {} ]\n", str1); // 反序列化
Person p1;
p1.ParseFromString(str);
fmt::print("Person1: name {0}, email {1}, id {2}", p1.name(), p1.email(), p1.id()); return 0;
}

Cpp 业务代码对应的 CMakeList.txt :

# 使用 VcPkg
# ./vcpkg install protobuf:[special-version] fmt:[special-version]
cmake_minimum_required(VERSION 3.24)
project(protobuf_tutorial) set(CMAKE_CXX_STANDARD 17) find_package(protobuf CONFIG REQUIRED)
find_package(fmt CONFIG REQUIRED) add_executable(protobuf_tutorial main.cpp
person.pb.cc) target_link_libraries(
protobuf_tutorial
PRIVATE
protobuf::libprotoc protobuf::libprotobuf protobuf::libprotobuf-lite
fmt::fmt
)

另一种可供的参考

# 未使用 VcPkg
cmake_minimum_required(VERSION 3.24)
project(protobuf_tutorial)
set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}") find_package(Protobuf REQUIRED)
include_directories(
include
${PROTOBUF_INCLUDE_DIRS}
)
add_library(addressbook_protobuf person.pb.cc)
add_executable(test_person test_person.cpp)
target_link_libraries(
test_person
addressbook_protobuf
${PROTOBUF_LIBRARIES}
)

一个更复杂一些的例子

package tutorial;

message Student {
required uint64 id = 1;
required string name =2;
optional string email = 3; enum PhoneType {
MOBILE = 0;
HOME = 1;
} message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
// Student_Test.cpp
#include <iostream>
#include <fstream>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/text_format.h>
#include <fmt/core.h>
#include "student.pb.h"
// #include "person.pb.h" using namespace tutorial; int main() {
// 作用 如果检测到版本不匹配时,程序将终止。
GOOGLE_PROTOBUF_VERIFY_VERSION;
// 给消息类 Student 对象 stu 赋值
Student stu;
stu.set_id(5720191825);
*stu.mutable_name() = "Koshkaaa";
stu.set_email("kannajiahe@gmail.com"); // 添加一个号码对象
Student::PhoneNumber *phoneNumber = stu.add_phone();
phoneNumber->set_number("0564-4762652");
phoneNumber->set_type(Student::HOME); // 再添加一个号码对象
Student::PhoneNumber *phoneNumber1 = stu.add_phone();
phoneNumber1->set_type(Student::MOBILE);
phoneNumber1->set_number("15813354925"); // 序列化到字符串中
std::string serializeStr;
stu.SerializeToString(&serializeStr);
fmt::print("Serialization Result: {}\n", serializeStr); // ========== 下面开始 反序列化 ===========
// 解析序列化的过程便称为 反序列化
Student deSerializeStudent;
if (!deSerializeStudent.ParseFromString(serializeStr)) {
fmt::print("Error to parse student.\n");
return -1;
} fmt::print("deSerializeStudent debugString: {}\n", deSerializeStudent.DebugString());
fmt::print("Student ID: {0}\nName: {1}\n", deSerializeStudent.id(),deSerializeStudent.name(), deSerializeStudent.email());
if (deSerializeStudent.has_email())
{
fmt::print("EMail: {}\n", deSerializeStudent.email());
}
for (const auto& phone: deSerializeStudent.phone()) {
switch ( phone.type() )
{
case Student_PhoneType_MOBILE:
{
fmt::print("Mobile Phone #{}\n", phone.number());
}
break;
case Student_PhoneType_HOME:
{
fmt::print("Home Phone #{}\n", phone.number());
}
break;
}
} // 对 protobuf 全局资源的清理工作
google::protobuf::ShutdownProtobufLibrary();
return 0;
}

扩展 Protocol Buffers

无论或早或晚,在你发布出使用 Protocol Buffers 的代码后,你必定会想“改进“ Protocol Buffers 的定义,即我们自定义消息的 proto 文件。

如果你想让新的 proto 文件向后兼容(backward-compatible),并且旧的 proto 文件能够向前兼容(forward-compatible),你一定希望如此,那么你在新的 proto 文件中就要遵守如下规则:

  1. 对已存在的任何字段,你都不能更改其标识(tag)号。

  2. 你绝对不能添加或删除任何 required 的字段。

  3. 你可以添加新的 optional 或 repeated 的字段,但是你必须使用新的标识(tag)号

    (例如,在这个 proto 文件中从未使用过的标识号——甚至于已经被删除过的字段使用过的标识号也不行)。

(有一些例外情况,但是它们很少使用。)

如果你遵守这些规则,老的代码将能很好地解析新的消息(message),并忽略掉任何新的字段。对老代码来说,已经被删除的 optional 字段将被赋予默认值,已被删除的 repeated 字段将是空的。新的代码也能够透明地读取旧的消息。

但是,请牢记心中:新的 optional 字段将不会出现在旧的消息中,所以你要么需要显式地检查它们是否由 has_ 前缀的函数置(set)了值,要么在你的 proto 文件中,在标识(tag)号的后面[default = value]提供一个合理的默认值。

如果没有为一个 optional 项指定默认值,那么就会使用与特定类型相关的默认值:对 string 来说,默认值是空字符串。对 boolean 来说,默认值是 false。对数值类型来说,默认值是0。

还要注意:如果你添加了一个新的 repeated 字段,你的新代码将无法告诉你它是否被留空了(被新代码),或者是否从未被置(set)值(被旧代码),这是因为它没有 has_ 标志。

优化小技巧(Optimization Tips)

Protocol Buffers 的 C++ 库已经做了极度优化。但是,正确的使用方法仍然会提高很多性能。下面是一些小技巧,用来提升 Protocol Buffers 库的最后一丝速度能力:

  1. 如果有可能,重复利用消息(message)对象。即使被清除掉,消息(message)对象也会尽量保存所有被分配来重用的内存。

    这样的话,如果你正在处理很多类型相同的消息以及一系列相似的结构,有一个好办法就是重复使用同一个消息(message)对象,从而使内存分配的压力减小一些。然而,随着时间的流逝,对象占用的内存也有可能变得越来越大,尤其是当你的消息尺寸(译者注:各消息内容不同,有些消息内容多一些,有些消息内容少一些)不同的时候,或者你偶尔创建了一个比平常大很多的消息(message)的时候。

    你应该自己通过调用 SpaceUsed 函数监测消息(message)对象的大小,并在它太大的时候删除它。

  2. 在多线程中分配大量小对象的内存的时候,你的操作系统的内存分配器可能优化得不够好。

    在这种情况下,你可以尝试用一下 Google’s tcmalloc。

高级用法(Advanced Usage)

Protocol Buffers 的作用绝不仅仅是简单的数据存取以及序列化。请阅读C++ API reference全文来看看你还能用它来做什么。

protocol 消息类所提供的一个关键特性就是反射。你不需要编写针对一个特殊的消息(message)类型的代码,就可以遍历一个消息的字段,并操纵它们的值,就像 XML 和 JSON 一样。“反射”的一个更高级的用法可能就是可以找出两个相同类型的消息之间的区别,或者开发某种“协议消息的正则表达式”,利用正则表达式,你可以对某种消息内容进行匹配。只要你发挥你的想像力,就有可能将 Protocol Buffers 应用到一个更广泛的、你可能一开始就期望解决的问题范围上。

“反射”是由 Message::Reflection interface 提供的。

【3rd Party】Cpp 中使用 Protobuf的更多相关文章

  1. cocos2d-x lua 中使用protobuf并对http进行处理

    cocos2d-x lua 中使用protobuf并对http进行处理 本文介绍 cocos2d-x lua 中使用http 和 基于cocos2d-x 对lua http的封装(部分ok) 本博客链 ...

  2. 在Wcf中应用ProtoBuf替代默认的序列化器

    Google的ProtoBuf序列化器性能的牛逼已经有目共睹了,可以把它应用到Socket通讯,队列,Wcf中,身为dotnet程序员一边期待着不久后Grpc对dotnet core的支持更期待着Wc ...

  3. template 不能分别在.h和.cpp中定义模板

    先上代码: #ifndef SEQLIST_H #define SEQLIST_H #include <iostream> ; template <typename type> ...

  4. webapi 中使用 protobuf

    相比json来说,好处是速度更快,带宽占用更小.其效果大致等于json+Gzip. 在webapi中使用protobuf的方法为: 引用nuget包 Install-Package protobuf- ...

  5. c/cpp中怎样切割字符串,相似于split的功能

    在python中,假设要求当前时间的unix时间戳,我特别喜欢这么用: import time timestr = time.time() timestamp = int(timestr.split( ...

  6. 在网络通讯中应用Protobuf

    在网络通讯中应用Protobuf Protobuf的设计非常适用于在网络通讯中的数据载体,它序列化出来的数据量少再加上以K-V的方式来存储数据,对消息的版本兼容性非常强:还有一个比较大的优点就是有着很 ...

  7. 怎样在Spark、Flink应用中使用Protobuf 3的包

    如果在在Spark.Flink应用中使用Protobuf 3的包,因为Spark默认使用的是2.5版本的包,提交任务时,可能会报如下异常: com.google.protobuf.CodedInput ...

  8. 在lua环境中使用protobuf

    最近在cocos2dx的项目中,需要在LUA脚本层使用protobuf协议.官方已经推出了很多种语言的版本.但唯独LUA版本不全.于是开始研究protobuf在LUA下的实现,将完整的过程记录了下来, ...

  9. 简单地说, cpp中的纯虚函数就是抽象类的具体实现

    简单地说, cpp中的纯虚函数就是抽象类的具体实现.包含了纯虚函数的类就是抽象类.

  10. 在 Java 中使用 protobuf

    在 Java 中使用 protobuf 从 https://github.com/google/protobuf/releases 下载编译器,并设置环境变量. 创建java项目添加protobuf- ...

随机推荐

  1. adb从基础到进阶

    一.adb的工作原理 adb是cs架构,由三部分组成,分别是client,server,daemon,他们的关系见下图 server是整个架构的核心 server负责接收client的指令,然后将指令 ...

  2. [编程]UML语言:理论之光与实践之惑

    UML介绍及现状 UML(统一建模语言)是软件工程领域中具有悠久历史的一种模型化语言工具.它通过标准化的图形符号体系,使得软件系统的蓝图能够被更直观地表达出来.UML诞生于20世纪90年代,经过多年积 ...

  3. 掌握HarmonyOS框架的ArkTs如何管理和共享状态数据

    本文分享自华为云社区<深入理解ArkTs中的AppStorage和LocalStorage>,作者:柠檬味拥抱 . ARKTS(Ark TypeScript)是HarmonyOS应用框架的 ...

  4. python数据类型元组、列表、集合、字典相互嵌套

    系统 Windows 10 专业工作站版22H2 软件 python-3.9.6-amd64.exe 拓展库: jupyter==1.0.0 notebook==7.0.6 1.元组嵌套 1.1 元组 ...

  5. [ARC145B] AB Game

    The game is played by Alice and Bob. Initially, there are $n$ stones. The players alternate turns, m ...

  6. Tampermonkey 编写一个首页跳转的脚本

    每次打开浏览器时,总是会跳到一个其他的网页上,关也关不掉,很烦,写一个脚本直接跳转 // ==UserScript== // @name 页面跳转 // @version 1.0.1 // @auth ...

  7. idea2020下载、安装、破解、配置

    idea2020下载.安装.破解.配置 idea2020下载 [推荐]官方下载地址:https://www.jetbrains.com/idea/download/other.html 进入后往下找, ...

  8. ASR项目实战-方案设计

    对于语音识别产品的实施方案,给出简易的业务流程,仅供参考. 如下流程图,可以使用如下两个站点查看. web chart Web Sequence Diagrams 文件转写 创建文件转写任务 客户应用 ...

  9. [VMware]ESXI下硬盘的两种直通方式

    文章来自:https://rmbz.net/archives/vmware-esxi-passthrough 最近再搞ESXI,把原来的"黑群晖"改成ESXI:因为群晖里有数据,为 ...

  10. Huggy Lingo: 利用机器学习改进 Hugging Face Hub 上的语言元数据

    太长不看版: Hub 上有不少数据集没有语言元数据,我们用机器学习来检测其语言,并使用 librarian-bots 自动向这些数据集提 PR 以添加其语言元数据. Hugging Face Hub ...