找回密码
 立即注册
查看: 461|回复: 0

1.1.5 Google ProtoBuffer详解

[复制链接]
发表于 2021-11-22 19:56 | 显示全部楼层 |阅读模式
1. Protocol Buffer 简介
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,
目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。
他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,
可以用于结构化数据串行化,或者说序列化。
它很适合做数据存储或 RPC 数据交换格式。

我们先来看看官方文档给出的定义和描述:
protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,
它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,
但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。
你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

简单来讲, ProtoBuf 是结构数据序列化[1] 方法,可简单类比于 XML[2],其具有以下特点:

    语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序

序列化[1]:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,
同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。
更为详尽的介绍可参阅 维基百科。
类比于 XML[2]:这里主要指在数据通信和数据存储应用场景中序列化方面的类比,
但个人认为 XML 作为一种扩展标记语言和 ProtoBuf 还是有着本质区别的。

ProtoBuf 可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
目前提供了 C++、Java、Python 三种语言的 API。

在分布式应用或者微服务架构中,各个服务之间通常使用json或者xml结构数据进行通信,
通常情况下,是没什么问题的,但是在高性能和大数据通信的系统当中,
如果有办法可以压缩数据量,提高传输效率,显然会给用户带来更快更流畅的体验。

2. 使用ProtoBuf
对 ProtoBuf 的基本概念有了一定了解之后,我们来看看具体该如何使用 ProtoBuf。
2.1 ProtoBuf的安装
2.1.1 操作命令
> git clone https://github.com/protocolbuffers/protobuf.git
> cd protobuf
> ./autogen.sh
> ./configure --prefix=/data/INSTALL_PATH
> make
> make install

2.1.2 错误提示及解决
1. 错误
> ./autogen.sh
+ mkdir -p third_party/googletest/m4
+ autoreconf -f -i -Wall,no-obsolete
./autogen.sh: line 37: autoreconf: command not found
解决:
> yum -y install  autoconf automake libtool

2. 错误
> ./configure --prefix=/data/INSTALL_PATH/
checking whether g++ supports C++11 features with -h std=c++0x... no
configure: error: *** A compiler with support for C++11 language features is required.
解决:
换用高版本的带C++11的CentOS;
或:
升级gcc到高级版本《CentOS6 升级gcc版本以支持C++11》https://blog.csdn.net/luyumiao1990/article/details/104582968

2.2 编译与运行示例程序
2.2.1 操作命令
> cd examples
> make
> ./add_person_cpp add_person_cpp.data
按提示输入内容
>  ./list_people_cpp add_person_cpp.data
  Person ID: 1231231
  Name: helloworld
  Home phone #: 2637849234
  Mobile phone #: 986789
  Updated: 2020-03-03T15:50:51

2.2.2 错误提示及解决
1. 错误
>  make
protoc $PROTO_PATH --cpp_out=. --java_out=. --python_out=. addressbook.proto
/bin/sh: protoc: command not found
make: *** [Makefile:28: protoc_middleman] Error 127
解决:
> vim /etc/profile
在最后添加:
export PATH=/data/INSTALL_PATH/bin:$PATH
保存并退出;
> source /etc/profile

2. 错误
> make
protoc $PROTO_PATH --cpp_out=. --java_out=. --python_out=. addressbook.proto
pkg-config --cflags protobuf  # fails if protobuf is not installed
Package protobuf was not found in the pkg-config search path.
Perhaps you should add the directory containing `protobuf.pc'
to the PKG_CONFIG_PATH environment variable
Package 'protobuf', required by 'virtual:world', not found
make: *** [Makefile:43: add_person_cpp] Error 1
解决:
export PKG_CONFIG_PATH=/data/INSTALL_PATH/lib/pkgconfig

3. 错误
> make
pkg-config --cflags protobuf  # fails if protobuf is not installed
-I/data/PJT-protobuf/PJT-google-protobuf/protobuf-install/include -pthread
c++ -std=c++11 http://add_person.cc http://addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf`
pkg-config --cflags protobuf  # fails if protobuf is not installed
-I/data/PJT-protobuf/PJT-google-protobuf/protobuf-install/include -pthread
c++ -std=c++11 http://list_people.cc http://addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf`
javac -cp $CLASSPATH AddPerson.java ListPeople.java com/example/tutorial/AddressBookProtos.java
/bin/sh: javac: command not found
make: *** [Makefile:67: javac_middleman] Error 127
解决:
> yum install -y java-1.8.0-openjdk-devel.x86_64

4.错误
> ./add_person_cpp -h
./add_person_cpp: error while loading shared libraries: libprotobuf.so.22: cannot open shared object file: No such file or directory
解决:
> vim /etc/profile
在最尾添加:
export LD_LIBRARY_PATH=/data/INSTALL_PATH/lib:$LD_LIBRARY_PATH
保存并退出;
> source /etc/profile

2.3 示例程序与分析
2.3.1  google原生示例程序
支持的语言及选择编译
示例程序支持多种语言,分别是:
# C++
cpp:    add_person_cpp    list_people_cpp  
# DART: Dart是谷歌开发的计算机编程语言,它被用于web、服务器、移动应用 [2]  和物联网等领域的开发。
dart:   add_person_dart   list_people_dart        
# Go:  Go(又称 Golang)是 Google的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。
# Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。
go:     add_person_go     list_people_go
gotest: add_person_gotest list_people_gotest
# Java
java:   add_person_java   list_people_java
# Python
python: add_person_python list_people_python

选择C++编译则是:
> make cpp
protoc $PROTO_PATH   --cpp_out=.   --java_out=.   --python_out=.    addressbook.proto
pkg-config --cflags protobuf  # fails if protobuf is not installed
-I/data/PJT-protobuf/PJT-google-protobuf/protobuf-install/include -pthread
c++ -std=c++11 http://add_person.cc http://addressbook.pb.cc -o add_person_cpp `pkg-config --cflags --libs protobuf`
pkg-config --cflags protobuf  # fails if protobuf is not installed
-I/data/PJT-protobuf/PJT-google-protobuf/protobuf-install/include -pthread
c++ -std=c++11 http://list_people.cc http://addressbook.pb.cc -o list_people_cpp `pkg-config --cflags --libs protobuf`

未编译前的文件:
addressbook.proto   # PB消息的定义
http://add_person.cc         # 接收输入并生成PB消息文件
http://list_people.cc           # 读取PB消息文件并导致
Makefile                   #  编译文件

编译后新增文件:
addressbook.pb.h    # 由 protoc生成的PB消息的头文件
http://addressbook.pb.cc   # 由 protoc生成的PB消息的定义文件

add_person_cpp      # 编译生成后的可执行程序
list_people_cpp        # 编译生成后的可执行程序

2.3.2  自添加新的示例程序
> mkdir pb-test
> cd pb-test
> vim example_desc.proto
// [START declaration]
syntax = "proto3";
// [END declaration]

// [START messages]
message  GslbDesc {
    message EmbeddedMessage {
        int32 int32Val   = 1;
        string stringVal = 2;
    }

    string stringVal                      = 1;
    bytes bytesVal                   = 2;

    EmbeddedMessage embeddedExample1 = 3;
    repeated int32 repeatedInt32Val  = 4;
    repeated string repeatedStringVal= 5;
}
// [END messages]
【保存并退出】
>/data/INSTALL_PATH/bin/protoc  --cpp_out=./   example_desc.proto
将生成头文件和函数定义文件,如下:
example_desc.pb.h
http://example_desc.ph.cc
> cat http://gslb_desc_test.cc
//
// Created by Hank on 2020-03-14.
//
#include <iostream>
#include <fstream>
#include <string>
#include "gslb_desc.pb.h"

int main() {
    GslbDesc gslb_desc;
    gslb_desc.set_stringval("hello,world");
    gslb_desc.set_bytesval("are you ok?");

    GslbDesc_EmbeddedMessage *embeddedExample2 = new GslbDesc_EmbeddedMessage();

    embeddedExample2->set_int32val(1);
    embeddedExample2->set_stringval("embeddedInfo");
    gslb_desc.set_allocated_embeddedexample1(embeddedExample2);

    gslb_desc.add_repeatedint32val(2);
    gslb_desc.add_repeatedint32val(3);
    gslb_desc.add_repeatedstringval("repeated1");
    gslb_desc.add_repeatedstringval("repeated2");

    std::string filename = "gslb_desc_val_result";
    std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
    if (!gslb_desc.SerializeToOstream(&output)) {
        std::cerr << "Failed to write gslb_desc." << std::endl;
        exit(-1);
    }
    return 0;
}

编译与链接生成可执行程序:
> g++ -Wall -g -c http://gslb_desc.pb.cc  -I../protobuf-install/include/  -L../protobuf-install/lib/ -lprotoc -lprotobuf -lprotobuf-lite
> g++ -Wall -g -c http://gslb_desc_test.cc  -I../protobuf-install/include/  -L../protobuf-install/lib/ -lprotoc -lprotobuf -lprotobuf-lite
> g++ -Wall -g -o gslb_desc_test gslb_desc.pb.o gslb_desc_test.o  -I../protobuf-install/include/  -L../protobuf-install/lib/ -lprotoc -lprotobuf -lprotobuf-lite

执行:
> ./gslb_desc_test

2.4 工程应用
上面讲的都是protobuf的原理和基本示例。
在实际的生产工程中应用时,
为了使pb通信的各方都使用统一的pb定义,可以将pb的定义和生成单独提取出来,作为一个通用的框架工程。
由它来统一定义pb字段,并生成所有pb要用的.http://pb.cc 和 .pb.h,
然后再由需要使用pb的工程引用这些 .http://pb.cc 和 .pb.h 。
在项目管理上也需要有人统一收集,添加,管理pb字段及这个框架工程,
可以避免一些字段不一致,版本冲突等问题。
第一步,创建 .proto 文件,定义数据结构,如下例1所示:
message Example1 {
    optional string stringVal = 1;
    optional bytes bytesVal = 2;
    message EmbeddedMessage {
        int32 int32Val = 1;
        string stringVal = 2;
    }
    optional EmbeddedMessage embeddedExample1 = 3;
    repeated int32 repeatedInt32Val = 4;
    repeated string repeatedStringVal = 5;
}我们在上例中定义了一个名为 Example1 的 消息,语法很简单,message 关键字后跟上消息名称:
message xxx {  }
之后我们在其中定义了 message 具有的字段,形式为:
message xxx {
  // 字段规则:required -> 字段只能也必须出现 1 次
  // 字段规则:optional -> 字段可出现 0 次或1次
  // 字段规则:repeated -> 字段可出现任意多次(包括 0)
  // 类型:int32、int64、sint32、sint64、string、32-bit ....
  // 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
  字段规则 类型 名称 = 字段编号;
}在上例中,我们定义了:

    类型 string,名为 stringVal 的 optional 可选字段,字段编号为 1,此字段可出现 0 或 1 次类型 bytes,名为 bytesVal 的 optional 可选字段,字段编号为 2,此字段可出现 0 或 1 次类型 EmbeddedMessage(自定义的内嵌 message 类型),名为 embeddedExample1 的 optional 可选字段,字段编号为 3,此字段可出现 0 或 1 次类型 int32,名为 repeatedInt32Val 的 repeated 可重复字段,字段编号为 4,此字段可出现 任意多次(包括 0)类型 string,名为 repeatedStringVal 的 repeated 可重复字段,字段编号为 5,此字段可出现 任意多次(包括 0)

关于 proto2 定义 message 消息的更多语法细节,
例如具有支持哪些类型,字段编号分配、import导入定义,reserved 保留字段等知识
请参阅 [翻译] ProtoBuf 官方文档(二)- 语法指引(proto2)
关于定义时的一些规范请参阅 [翻译] ProtoBuf 官方文档(四)- 规范指引

第二步,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
最终生成的代码将提供类似如下的接口:
bool SerializeToString(string* output) const;  // 将对象序列化为二进制字符串
bool ParseFromString(const string& data);   // 解析一个二进制字符串

bool SerializeToOstream(ostream* ouput) const; //将对象序列化为C++ ostream
bool ParseFromIstream(istream* input);  // 解析C++ istream
例子-序列化和解析接口

void clear_int32_val();
void set_int32val(::google::protobuf::int32 value); // 设置int32val 值
::google::protobuf::int32 int32val() const; // 获取 int32val 值

void clear_stringval();
void set_stringval(const char* value); // 设置 stringval值
const ::std::string& stringval() const;  // 获取 stringval值
例子-protoc 生成接口

第三步,调用接口实现序列化、反序列化以及读写
针对第一步中例1定义的 message,我们可以调用第二步中生成的接口,实现测试代码如下:
//
// Created by yue on 18-7-21.
//
#include <iostream>
#include <fstream>
#include <string>
#include "single_length_delimited_all.pb.h"

int main() {
    Example1 example1;
    example1.set_stringval("hello,world");
    example1.set_bytesval("are you ok?");

    Example1_EmbeddedMessage *embeddedExample2 = new 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 = "single_length_delimited_all_example1_val_result";
    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;
}关于 protoc 的使用以及接口调用的更多信息可参阅 [翻译] ProtoBuf 官方文档(九)- (C++开发)教程
关于例1的完整代码请参阅 源码:protobuf 例1。其中的 single_length_delimited_all.* 为例子相关代码和文件。
因为此系列文章重点在于深入 ProtoBuf 的编码、序列化、反射等原理,关于 ProtoBuf 的语法、使用等只做简单介绍,更为详见的使用教程可参阅我翻译的系列官方文档。

3. 一个简单的例子
3.1 安装 Google Protocol Buffer
在网站 http://code.google.com/p/protobuf/downloads/list上可以下载 Protobuf 的源代码。
然后解压编译安装便可以使用它了。
安装步骤如下所示:
tar -xzf protobuf-2.1.0.tar.gz
cd protobuf-2.1.0
./configure --prefix=$INSTALL_DIR
make
make check
make install

3.2 关于简单例子的描述
我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序。该程序由两部分组成:
   第一部分被称为 Writer,    负责将一些结构化的数据写入一个磁盘文件,
   第二部分叫做 Reader,     负责从该磁盘文件中读取结构化数据并打印到屏幕上。
准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:
    ID,为一个整数类型的数据Str,这是一个字符串

3.3 书写 .proto 文件
首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,
在 protobuf 的术语中,结构化数据被称为 Message。
proto 文件非常类似 java 或者 C 语言的数据定义。
代码清单 1 显示了例子应用中的 proto 文件内容。
清单 1. proto 文件
package lm;
message helloworld
{
   required int32     id = 1;  // ID
   required string    str = 2;  // str
   optional int32     opt = 3;  //optional field
}

一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:
packageName.MessageName.proto

在上例中,
package 名字叫做 lm,
定义了一个消息 helloworld,
该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。

3.4 编译 .proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。
假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:
            protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件
http://lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。
诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

3.5  编写 writer 和 Reader
如前所述,Writer 将把一个结构化数据写入磁盘,以便其他人来读取。
假如我们不使用 Protobuf,其实也有许多的选择。
一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。
转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。
这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写 Reader 的那个人的要求比较高,Reader 的作者必须了解 Writer 的细节。
比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。
这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以正确读取。
但分隔符也许还会引起其他的什么问题。
最后我们发现一个简单的 Helloworld 也需要写许多处理消息格式的代码。
如果使用 Protobuf,那么这些细节就可以不需要应用程序来考虑了。

使用 Protobuf,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,
经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。
对于本例,类名为 lm::helloworld。
Writer 需要 include 该头文件,然后便可以使用这个类了。
现在,在 Writer 代码中,将要存入磁盘的结构化数据由一个 lm::helloworld 类的对象表示,
它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫 field。
当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,
我们可以将这个字节序列写入磁盘。

对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。
这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,
因此,我们不如放心将这类事情交给 Protobuf 吧。
程序清单 2 演示了 Writer 的主要代码,您一定会觉得很简单吧?
清单 2. Writer 的主要代码
#include "lm.helloworld.pb.h"

int main(void)
{
  lm::helloworld msg1;
  msg1.set_id(101);
  msg1.set_str(“hello”);
  // Write the new address book back to disk.
  fstream output("./log", ios::out | ios::trunc | ios::binary);
  if (!msg1.SerializeToOstream(&output)) {
      cerr << "Failed to write msg." << endl;
      return -1;
  }         
  return 0;
}

Msg1 是一个 helloworld 类的对象,set_id() 用来设置 id 的值。SerializeToOstream 将对象序列化后写入一个 fstream 流。

代码清单 3 列出了 reader 的主要代码。
清单 3. Reader
#include "lm.helloworld.pb.h"

void ListMsg(const lm::helloworld & msg) {
  cout << msg.id() << endl;
  cout << msg.str() << endl;
}
int main(int argc, char* argv[]) {
  lm::helloworld msg1;
  {
    fstream input("./log", ios::in | ios::binary);
    if (!msg1.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }
  ListMsg(msg1);
  …
}

同样,Reader 声明类 helloworld 的对象 msg1,
然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。
此后,ListMsg 中采用 get 方法读取消息的内部信息,并进行打印输出操作。

3.6 运行结果
运行 Writer 和 Reader 的结果如下:
>writer
>reader
101
Hello

Reader 读取文件 log 中的序列化信息并打印到屏幕上。本文中所有的例子代码都可以在附件中下载。您可以亲身体验一下。
这个例子本身并无意义,但只要您稍加修改就可以将它变成更加有用的程序。
比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。
而存储和交换正是 Protobuf 最有效的应用领域。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 13:35 , Processed in 0.219322 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表