初学者如何编辑protobuf文档,然后编译出.cc和.h文件。再调用生成的文件创建序列化字节流。
一、安装环境
首先下载protobuf的安装包,我这里使用的是protobuf-cpp-3.8.0.tar.gz
1. 解压安装包
tar zxvf protobuf-cpp-3.8.0.tar.gz
2.进入解压后的文件夹
cd protobuf-3.8.0
3.生产Makefile文件
./configure
4.执行make编译
make
5.安装
sudo make install
6.更新动态链接库为系统所共享
sudo ldconfig
7.显示版本
protoc --version
二、编辑proto文件
在文件夹proto中创建如下三个文件。
包含的文件,其中用到两个.proto文件是为了演示多个文件相互包含的应用,因为实际工程中不会把所有的定义写在一个文件中。所有一切为了练习。
create.sh是用于编译X.proto文件的脚本文件。
projectXX.Personinfo.proto是定义个人信息结构
projectXX.BaseDefine.proto是定义基本信息结构
create.sh内容讲解
#!/bin/sh
# proto文件在哪里
SRC_DIR=./
# .h .cc输出到哪里
DST_DIR=../
#C++
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.proto
1.首先定义了一个源文件路径‘./’。这实际就是脚本文件所在的当前路径。
2.定义了一个输出文件路径'../'。这实际就是脚本文件所在的路径上一级路径。
3.关键是最后一行。protoc 只指令本身;-I=$SRC_DIR指定要编译的文件来自哪个目录;--cpp_out=$DST_DIR指定编译后的文件输出的目录;$SRC_DIR/*.proto 表示要前面指定的目录(-I=$SRC_DIR)中查找哪些文件进行编译,这里的意识就是查找"./*.proto"当前路径下所有.proto结尾的文件进行编译,然后输出到上一级目录中。下图看到还没有编译前,还没有proto的.cc和.h文件。
文件projectXX.BaseDefine.proto内容
syntax = "proto3";
package projectXX.BaseDefine;
//option optimize_for = LITE_RUNTIME;
enum PhoneType{
PHONE_DEFAULT = 0x0;
PHONE_HOME = 0x0001; // 家庭电话
PHONE_WORK = 0x0002; // 工作电话
}
文件解释:第一行是指定版本为proto3,目前基本都是用这个版本了。第二行是定义此文件编译目标文件的包名称。如果其他模块需要使用该文件中定义的结构,就需要先引入此包名称。相当于C/C++中的名字空间。第三行表示使用protobuf的核心库来生产代码,运行速度快。第四行(其实称第四部分更合理)就是定义了一个 PhoneType枚举类型。这个和C语言中一样的理解,即限制该类型数据的取值范围。此文件中的只定义了一个枚举类型,就是就是为了练习proto多文件是如何相互使用的。
文件projectXX.personinfo.proto内容
syntax = "proto3"; // syntax 版本2/3是不一样,默认是proto2
package projectXX.Personinfo; // package 生成对应的C++命名空间 projectXX::personinfo::
import "projectXX.BaseDefine.proto"; // import 引用其他proto文件
import "google/protobuf/timestamp.proto"; // proto自己带的已经写好的proto文件也可以导入
//option optimize_for = LITE_RUNTIME;
// message关键字 代表一个对象
message Phone{
string number = 1; // = 1是什么?默认值
projectXX.BaseDefine.PhoneType phone_type = 2;
}
message Person{
string name = 1;
int32 age = 2;
Phone phone = 3;
google.protobuf.Timestamp last_updated = 4; // import "google/protobuf/timestamp.proto"
}
解释:文件头部信息就不在解释了,关键是在改文件中定义了两个message结构。proto中message修饰的关键词相当于C++中的类。所有该文件会产生一个Phone类和一个Person类。看后面test.cpp中是如何使用的,就会比较清楚了。
三、生成proto的.cc和.h文件。(这里是生产C++的代码)
如果create.sh没有执行属性,请先添加执行属性执行chmod +x create.sh
在执行./create.sh
查看上级文件夹中多出了4个文件,即时proto的编译结果。
projectXX.BaseDefine.pb.cc和projectXX.BaseDefine.pb.h就是对应文件projectXX.BaseDefine.proto生成的代码。projectXX.Personinfo.pb.cc和projectXX.Personinfo.pb.h就是对应模块projectXX.Personinfo.proto生成的代码。
这里在解释一下,为什么我proto文件名称用这么多的.隔开了,而不是我们通常的文件命名方式只有文件名称和扩展名之间才有一个.符号隔开呢。我认为就是大家工程实践中一种预定,并不proto的规定。一般来说在实际的项目中,第一部分指定项目的名称,我这里用projectXX。第二部分指定改文件包含的信息,我这里用Personinfo。最后一部分就是指明改文件是proto的文件,方便大家识别。还有一点非常重要,就是package projectXX.personinfo;一般来说每个文件的包名,都是于文件名相同,当我们程序中要使用projectXX.personinfo包中定义的类时,我们看到类的包名就可以对应的找到改类是在哪个proto文件中定义的了。设想一下,包名和proto文件名不相同时,我们要根据一个类的包名,要怎么去查找该类的定义呢。
四、使用
实例中用到CMakeList.txt。 cmake的使用这里就不展开说明,通常执行cmake时都是在build文件夹中进行,目的是为了将编译的各种输出文件和原文件隔离开。
文件内容如下。
cmake_minimum_required(VERSION 2.6)
project (test_sf)
ADD_DEFINITIONS(-W -Wall -D_REENTRANT -D_FILE_OFFSET_BITS=64 -DAC_HAS_INFO
-DAC_HAS_WARNING -DAC_HAS_ERROR -DAC_HAS_CRITICAL -DTIXML_USE_STL
-DAC_HAS_DEBUG -DLINUX_DAEMON -std=c++11)
set(test_srcs
projectXX.BaseDefine.pb.cc
projectXX.Personinfo.pb.cc
)
add_executable(test_sf test.cpp ${test_srcs} )
TARGET_LINK_LIBRARIES(test_sf protobuf pthread)
第二个文件就是test.cpp文件内容如下。
#include <ctime>
#include <fstream>
#include <google/protobuf/util/time_util.h>
#include <iostream>
#include <string>
#include "projectXX.Personinfo.pb.h"
using namespace std;
using google::protobuf::util::TimeUtil;//使用proto自带的命名空间中的TimeUtil,用于获取时间
// This function fills in a Person message based on user input.
void PromptForAddress(projectXX::Personinfo::Person* person) {
cout << "Enter name: ";
getline(cin, *person->mutable_name());
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
// mutable_用于获取单独的message定义的对象,如果存在怎返回对象指针。如果不存在则新建一个对象并返回对象指针,用户不需要去手动释放此处新建的对象。
projectXX::Personinfo::Phone *phone = person->mutable_phone();
phone->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone->set_phone_type(projectXX::BaseDefine::PHONE_MOBILE);
} else if (type == "home") {
phone->set_phone_type(projectXX::BaseDefine::PHONE_HOME);
} else if (type == "work") {
phone->set_phone_type(projectXX::BaseDefine::PHONE_WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
*person->mutable_last_updated() = TimeUtil::SecondsToTimestamp(time(NULL)); // 设置时间
}
int main(int argc, char* argv[]) {
// 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;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
projectXX::Personinfo::Person person_info;
{
// Read the existing Person. 本地文件保存的数据
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!person_info.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
cout << "person_info name " <<person_info.name() << endl;
projectXX::Personinfo::Phone *phone = person_info.mutable_phone();
string number = phone->number();
cout << "person_info number " <<number << endl;
}
// Add an person_info. 新建一个对象Person
PromptForAddress(&person_info);
int isize = person_info.ByteSize();
std::cout << "person_info size = " << isize << std::endl;
{
// Write the new people back to disk. 个人信息
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!person_info.SerializeToOstream(&output)) {
cerr << "Failed to write Person." << endl;
return -1;
}
}
cout << "person_info name " <<person_info.name() << endl;
projectXX::Personinfo::Phone *phone = person_info.mutable_phone();
string number = phone->number();
cout << "person_info name " <<number << endl;
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
proto简单语法解释:
如果proto结构体的变量是基础变量,比如int、string等等,那么set的时候直接调用set_xxx即可。
如果变量是自定义类型(也就是message嵌套),那么C++的生成代码中,就没有set_xxx函数名,取而代之的是三个函数名:
set_allocated_xxx()
release_xxx()
mutable_xxx()
使用set_allocated_xxx()来设置变量的时候,变量不能是普通栈内存数据,
必须是手动new出来的指针,至于何时delete,就不需要调用者关心了,
protobuf内部会自动delete掉通过set_allocated_设置的内存;
release_xxx()是用来取消之前由set_allocated_xxx设置的数据,
调用release_xxx以后,protobuf内部就不会自动去delete这个数据;
mutable_xxx()返回分配内存后的对象,如果已经分配过则直接返回,如果没有分配则在内部分配,建议使用mutable_xxx
测试代码解释:
根据执行程序是输入的参数argv[1]作为文件,已二进制读取到变量input中。在把读取到的二进制字节流传入person_info对象的反序列化函数person_info.ParseFromIstream(&input)中。改函数的作用就是将二进制字节转化为person_info对象。然后将person_info对象的name和number两个变量打印到屏幕上。这部分主要是测试将proto对象反序列化。
然后是修改开始创建的 person_info对象内容。练习如何设置person_info对象的name和number等信息,信息由用户键盘输入后保存到person_info对象。在获取person_info对象的大小,然后将person_info对象序列化后存储到ch数组中并且打印出来。我测试写入的对象大小是32个字节,内容也打印如下。
这里是将用户输入的person_info对象序列化后存储到一个文件中,即保存到文件。然后再练习如何获取person_info对象的各字段信息。
总结:
本程序测试了创建proto对象,用proto对象的反序列化函数将二进制字节转换为proto对象。然后是如何调用proto的设置函数设置对象的值。已经如何使用proto对象的序列化函数将对象转化为二进制存入数组或序列化函数转换为二进制存入文件,已经如何获取proto对象的各字段的数据。
补充一点:其实这里用到两个序列化函数person_info.SerializeToArray(ch,isize);
person_info.SerializeToOstream(&output);其实同一个proto对象序列化后的二进制值一定是相同的,不同的序列化函数只是给用户方便存入不同的地方。这里一个是存入数组,一个是存入文件。
下图就是数组的内容和文件的内容,是完全相同的32个字节(备注:存入的文件需要可以打开二进制文本软件才可以查到到)。
参考:
本文中很多代码来自腾讯课堂的零声教育,有兴趣的同学可以去了解很多内容。