protobuf协议原理及实现,基于c++

一.protobuf协议简介

1.1 protobuf协议简介

  Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。它不依赖于语言和平台并且可扩展性极强。
  同XML相比,Protocol buffers在序列化结构化数据方面有许多优点:

  1. 更简单;
  2. 数据描述文件只需原来的1/10至1/3;
  3. 解析速度是原来的20倍至100倍;
  4. 减少了二义性;
  5. 生成了更容易在编程中使用的数据访问;
  6. 支持多种编程语言;

1.2 数据交互xml、json、protobuf格式比较

1、json: 一般的web项目中,最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支持。

2、xml: 在webservice中应用最为广泛,但是相比于json,它的数据更加冗余,因为需要成对的闭合标签。json使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。

3、protobuf:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。

相对于其它,protobuf更具有优势 :
1:序列化后体积相比Json和XML很小,适合网络传输
2:支持跨平台多语言
3:消息格式升级和兼容性还不错
4:序列化反序列化速度很快,快于Json的处理速速

1.3 关于 ProtoBuf 的一些思考

转自:https://www.jianshu.com/p/a24c88c0526a

  官方文档以及网上很多文章提到 ProtoBuf 可类比 XML 或 JSON。那么 ProtoBuf 是否就等同于 XML 和 JSON 呢,它们是否具有完全相同的应用场景呢?
  个人认为如果要将 ProtoBuf、XML、JSON 三者放到一起去比较,应该区分两个维度。一个是数据结构化,一个是数据序列化。这里的数据结构化主要面向开发或业务层面,数据序列化面向通信或存储层面,当然数据序列化也需要“结构”和“格式”,所以这两者之间的区别主要在于面向领域和场景不同,一般要求和侧重点也会有所不同。数据结构化侧重人类可读性甚至有时会强调语义表达能力,而数据序列化侧重效率和压缩。

  从这两个维度,我们可以做出下面的一些思考:
  XML 作为一种扩展标记语言,JSON 作为源于 JS 的数据格式,都具有数据结构化的能力。例如 XML 可以衍生出 HTML (虽然 HTML 早于 XML,但从概念上讲,HTML 只是预定义标签的 XML),HTML 的作用是标记和表达万维网中资源的结构,以便浏览器更好的展示万维网资源,同时也要尽可能保证其人类可读以便开发人员进行编辑,这就是面向业务或开发层面的数据结构化。
  再如 XML 还可衍生出 RDF/RDFS,进一步表达语义网中资源的关系和语义,同样它强调数据结构化的能力和人类可读。
  JSON 也是同理,在很多场合更多的是体现了数据结构化的能力,例如作为交互接口的数据结构的表达。在 MongoDB 中采用 JSON 作为查询语句,也是在发挥其数据结构化的能力。
  当然,JSON、XML 同样也可以直接被用来数据序列化,实际上很多时候它们也是这么被使用的,例如直接采用 JSON、XML 进行网络通信传输,此时 JSON、XML 就成了一种序列化格式,它发挥了数据序列化的能力。但是经常这么被使用,不代表这么做就是合理。实际将 JSON、XML 直接作用数据序列化通常并不是最优选择,因为它们在速度、效率、空间上并不是最优。换句话说它们更适合数据结构化而非数据序列化。
  扯完 XML 和 JSON,我们来看看 ProtoBuf,同样的 ProtoBuf 也具有数据结构化的能力,其实也就是上面介绍的 message 定义。我们能够在 .proto 文件中,通过 message、import、内嵌 message 等语法来实现数据结构化,但是很容易能够看出,ProtoBuf 在数据结构化方面和 XML、JSON 相差较大,人类可读性较差,不适合上面提到的 XML、JSON 的一些应用场景。
  但是如果从数据序列化的角度你会发现 ProtoBuf 有着明显的优势,效率、速度、空间几乎全面占优,看完后面的 ProtoBuf 编码的文章,你更会了解 ProtoBuf 是如何极尽所能的压榨每一寸空间和性能,而其中的编码原理正是 ProtoBuf 的关键所在,message 的表达能力并不是 ProtoBuf 最关键的重点。所以可以看出 ProtoBuf 重点侧重于数据序列化 而非 数据结构化。

二.protobuf库安装

  在github可下载最新版的,可能有些框架不支持最新版,注意下载自己需要的版本。

下载地址:https://github.com/protocolbuffers/protobuf/releases

  进入下载页面后(如下图所示),选择自己需要的版本,这里选择protobuf-cpp-3.21.6.tar.gz,注意此处cpp仅包含c++版本,若需对其他语言支持,则可下载相应的版本,或下载all包含对多个语言的支持。

在这里插入图片描述
下载:

wget https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protobuf-cpp-3.21.6.tar.gz

解压:

tar -zxvf protobuf-cpp-3.21.6.tar.gz

编译安装:

./configure
make
make install

检查是否安装成功(查看protoc版本):

protoc --version

三.protobuf库使用

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

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

test.proto:

// 例1: 在 xxx.proto 文件中定义 Example1 message
syntax = "proto2";
package test;   //指明namespace

message Example1 {
    optional string stringVal = 1;
    optional bytes bytesVal = 2;
    message EmbeddedMessage {
        optional int32 int32Val = 1;
        optional string stringVal = 2;
    }
    optional EmbeddedMessage embeddedExample1 = 3;
    repeated int32 repeatedInt32Val = 4;
    repeated string repeatedStringVal = 5;
}

  .proto 语法类似于 C++ 或 Java。让我们浏览文件的每个部分,看看它们的作用。
  .proto 文件以 package 声明开头,这有助于防止不同项目之间的命名冲突。在 C++ 中,生成的类将放在与包名匹配的 namespace (命名空间)中。
  接下来,是相关的 message 定义。message 只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 bool、int32、float、double 和 string。你还可以使用其他 message 类型作为字段类型在消息中添加更多结构 - 在上面的示例中,Example1 包含 EmbeddedMessage message 。

message xxx {
  // 字段规则:required -> 字段只能也必须出现 1 次
  // 字段规则:optional -> 字段可出现 0 次或1次
  // 字段规则:repeated -> 字段可出现任意多次(包括 0)
  // 类型:int32、int64、sint32、sint64、string、32-bit ....
  // 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
  字段规则 类型 名称 = 字段编号;
}

  每个元素上的 “=1”,“=2” 标记表示该字段在二进制编码中使用的唯一 “标记”。标签号 1-15 比起更大数字需要少一个字节进行编码,因此以此进行优化,你可以决定将这些标签用于常用或重复的元素,将标记 16 和更高的标记留给不太常用的可选元素。repeated 字段中的每个元素都需要重新编码 Tag,因此 repeated 字段特别适合使用此优化。“repeated 字段中的每个元素都需要重新编码 Tag”,指的应该是 string 等类型的 repeated 字段。

  必须使用以下修饰符之一注释每个字段:

  • required: 必须提供该字段的值,否则该消息将被视为“未初始化”。如果是在调试模式下编译 libprotobuf,则序列化一个未初始化的 message 将将导致断言失败。在优化的构建中,将跳过检查并始终写入消息。但是,解析未初始化的消息将始终失败(通过从解析方法返回 false)。除此之外,required 字段的行为与 optional 字段完全相同。
  • optional: 可以设置也可以不设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,你可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,使用系统默认值:数字类型为 0,字符串为空字符串,bools 为 false。对于嵌入 message,默认值始终是消息的 “默认实例” 或 “原型”,其中没有设置任何字段。调用访问器以获取尚未显式设置的 optional(或 required)字段的值始终返回该字段的默认值。
  • repeated: 该字段可以重复任意次数(包括零次)。重复值的顺序将保留在 protocol buffer 中。可以将 repeated 字段视为动态大小的数组。

  对于 required 类型你应该时时刻刻留意,将字段设置为 required 类型是一个值得谨慎思考的事情。如果你希望在某个时刻停止写入或发送 required 字段,则将字段更改为 optional 字段会有问题 - 旧读者会认为没有此字段的邮件不完整,可能会无意中拒绝或删除这些消息。你应该考虑为你的 buffers 编写特定于应用程序的自定义验证例程。谷歌的一些工程师得出的结论是,使用 required 弊大于利; 他们更喜欢只使用 optional 和 repeated。但是,这种观点还未得到普及。

  可以在Protocol Buffer 语法指南中找到编写 .proto 文件(包括所有可能的字段类型)的完整指南。不要去寻找类似于类继承的工具(设计),protocol buffers 不会这样做。

第二步,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 --cpp_out=. test.proto       # 真实执行

这将在指定的目标目录中生成以下文件:

test.pb.h: 类声明的头文件
test.pb.cc:类实现

  其中,test.pb.h作为头文件提供外部调用的接口;test.pb.cc作为类实现的源文件;
  让我们看看一些生成的代码,看看编译器为你创建了哪些类和函数。如果你查看test.pb.h,你会发现你在 test.proto 中指定的每条 message 都有一个对应的类。仔细观察 Example1 类,你可以看到编译器已为每个字段生成了访问器。

第三步,调用接口实现序列化、反序列化以及读写

  针对第一步中例定义的 message,我们可以调用第二步中生成的接口,实现测试代码如下:
main.cpp

//
// Created by hututu on 22-9-22
//
#include <iostream>
#include <fstream>
#include <string>
#include "test.pb.h"
 
int main() {

    // Verify that the version of the library that we linked against is
    // compatible with the version of the headers we compiled against.
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    test::Example1 example1;   //注意:此处应该指名namespace = test(在test.proto中指定了)
    example1.set_stringval("hello,world");
    example1.set_bytesval("are you ok?");
 
    test::Example1_EmbeddedMessage *embeddedExample2 = new test::Example1_EmbeddedMessage();
 
    embeddedExample2->set_int32val(1);
    embeddedExample2->set_stringval("embeddedInfo");
    example1.set_allocated_embeddedexample1(embeddedExample2);
 
    example1.add_repeatedint32val(2);
    example1.add_repeatedint32val(3);
    example1.add_repeatedstringval("repeated1");
    example1.add_repeatedstringval("repeated2");
 
    std::string filename = "protobuf_file.txt";
    std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
    if (!example1.SerializeToOstream(&output)) {
        std::cerr << "Failed to write example1." << std::endl;
        exit(-1);
    }
 
    return 0;
}

编译:

g++ main.cpp test.pb.cc -lprotobuf -std=c++17

执行:

./a.out

执行结果:将example1对象的数据按照protobuf协议写入“protobuf_file.txt”文件中
在这里插入图片描述
  请注意 GOOGLE_PROTOBUF_VERIFY_VERSION 宏。在使用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法 - 尽管不是绝对必要的。它验证你没有意外链接到与你编译的头文件不兼容的库版本。如果检测到版本不匹配,程序将中止。请注意,每个 .pb.cc 文件在启动时都会自动调用此宏
  另请注意在程序结束时调用 ShutdownProtobufLibrary()。所有这一切都是删除 Protocol Buffer 库分配的所有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责回收其所有内存。但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用 Protocol Buffers 来清理所有内容。
  参考链接:ProtoBuf 官方文档(九)- (C++开发)教程

3.1 优化技巧

  C++ Protocol Buffers 已经做了极大优化。但是,正确使用可以进一步提高性能。以下是压榨最后一点速度的一些提示和技巧:
  尽可能重用 message 对象 。message 会为了重用尝试保留它们分配的任何内存,即使它们被清除。因此,如果你连续处理具有相同类型和类似结构的许多 message,则每次重新使用相同的 message 对象来加载内存分配器是个好主意。但是,随着时间的推移,对象会变得臃肿,特别是如果你的 message 在 “形状” 上有所不同,或者你偶尔构造的 message 比平时大得多。你应该通过调用 SpaceUsed 方法来监控邮件对象的大小,一旦它们变得太大就删除它们。
  你的系统内存分配器可能没有针对从多个线程分配大量小对象这种情况进行良好优化。请尝试使用 Google 的 tcmalloc

3.2 高级用法

  Protocol buffers 的用途不仅仅是简单的访问器和序列化。请务必浏览 C++ API 参考,以了解你可以使用它们做些什么。
  Protocol buffers 类提供的一个关键特性是反射。你可以迭代 message 的字段并操纵它们的值,而无需针对任何特定的 message 类型编写代码。使用反射的一种非常有用的应用是将 protocol messages 转换为其他编码,例如 XML 或 JSON。更高级的反射用法可能是找到两个相同类型的 message 之间的差异,或者开发一种 “protocol messages 的正则表达式”,你可以在其中编写与某些 message 内容匹配的表达式。如果你运用自己的想象力,可以将 Protocol Buffers 应用于比你最初预期更广泛的问题!

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值