google protobuf是一个灵活的、高效的用于序列化数据的协议。相比较XML和JSON格式,protobuf更小、更快、更便捷。google protobuf是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可以编译成Java、python、C++、C#、Go等代码,然后就可以直接使用,不需要再写其他代码,自带有解析的代码。
Download
Installatin
假定下载的是全版本protobuf-3.4.x.zip。则安装步骤如下:
安装runtimes:
unzip protobuf-3.4.x.zip
cd protobuf-3.4.x/
./autogen.sh
./configure
make
make check
sudo make install
以上命令如果出错,则根据提示执行如下的对应命令:
sudo apt install curl
sudo apt install autoconf
sudo apt install libtool
安装Python需要的库:
cd python/
python setup.py build
python setup.py test
sudo python setup.py install
如果以上步骤出错,则根据提示执行如下的对应命令:
sudo apt install python-pip
pip install --upgrade pip
pip install setuptools
路径
usr/local/bin
usr/local/lib,
usr/local/include
是也系统默认路径之一,所以到这一步就可以使用protobuf了
$ protoc -I=./ --cpp_out=./ test.proto
到你的test.proto文件所在目录使用命令
protoc -I=./ --cpp_out=./ 生成C++版本的协议文件
一切OK的话,你回在当前目录看到.h和.cc文件
在/etc/profile 或者用户目录 ~/.bash_profile
添加下面内容
############################### add protobuf lib path ########################################
#(动态库搜索路径) 程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib/
#(静态库搜索路径) 程序编译期间查找动态链接库时指定查找共享库的路径
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib/
#执行程序搜索路径
export PATH=$PATH:/usr/local/protobuf/bin/
#c程序头文件搜索路径
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/protobuf/include/
#c++程序头文件搜索路径
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/protobuf/include/
#pkg-config 路径
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
######################################################################################
如果出现找不到符号和链接错误请记得加上链接选项 -lprotobuf 。并确认你的静态库路径是否生效了 echo $LIBRARY_PATH
概述
syntax
// rs.UserInfo.proto
syntax = "proto2";
package rs;
//import "xxx.proto";
message UserInfo{
optional uint32 id = 1;
required string name = 2;
required string pswd = 3;
enum Type{
ROOT = 0,
ADMIN = 1,
USUAL = 2,
VISITOR = 4,
}
required Type type = 4;
repeated string phone = 5;
repeated string email = 6;
}
basic
- syntax表示使用语法版本
- package类似c++中的namespace,归类并避免命名冲突
- import引入其他文件
- message用于定义结构体
- enum用于枚举
字段修饰符
proto2:
- required:必须的
- optional:可选的
- repeated:重复的(类比数组,可以为0)
proto3:
- singular:单一的(默认,可以省略)
- repeated:重复的
字段类型
varint
Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。
编码规则:
使用Little-Endian小端字节序;
每个字节最高位是标志位,0表示结束,1表示继续;后7为保存数据;
因为负数最高位为1,所以先要进行int2uint转换,采用ZigZag方法实现;
uint32 Zig(int32 value){
return (uint)((value << 1) ^ (value >> 31));
}
int32 Zag(uint32 value){
return (-(value & 0x01)) ^ ((value >> 1) & ~( 1<< 31));
}
message存储格式
message采用key[-lenght]-value存储格式
key = field_num << 3 | data_type
对于Lenght-delimited即data_type == 2类型数据紧跟着length信息,标识value的长度
key、lenght、data_type == 1的value都采用varint编码
生成代码
使用protoc编译proto文件即可自动生成代码,下面以cpp为例
protoc UserInfo.proto --cpp_out=cpp
在cpp/rs目录下可以发现生成了两个文件UserInfo.pb.h,UserInfo.pb.cc
下面我们分析下生成代码提供的类和方法
基类::google::protobuf::Message
序列化
SerializedAsString(),SerializedToString(std::string*):序列化为std::string
SerializedToArray(void*,int):序列化为byte数组
SerializedToOstream(ostream*):序列化到输出流
反序列化
ParseFromString(std::string& data):从字符串中反序列化
ParseFromArray(const void *,int):从字节序中反序列化
ParseFromIstream(istream*):从输入流中反序列化
getter、setter
all:
clear_field():重置字段
非repeated字段:
field():获取字段值,只读
set_field():设置字段
mutable_filed():返回字段指针,用于修改
repeated字段:
filed(int):获取字段对应索引值,只读
set_filed(int,x):设置字段对应索引值
mutable_filed(int):返回字段对应索引指针,用于修改
add_filed():添加字段
filed_size():返回字段元素个数
TestCase
#include "rs/UserInfo.pb.h"
using namespace rs;
int main(int argc, char* argv[]){
UserInfo user;
user.set_id(1);
user.set_name("hw");
user.set_pswd("123456");
user.set_type(UserInfo_Type_ADMIN);
user.add_phone("13012345678");
std::string strUser = user.SerializeAsString();
user.PrintDebugString();
printf("protobuf bytes:%d\n\n", strUser.size());
UserInfo user1;
user1.ParseFromString(strUser);
user1.PrintDebugString();
char json[1024];
snprintf(json, 1024, "{id:%d,name:%s,pswd:%s,type:%d,phone:[%s]}",
user1.id(), user1.name().c_str(), user1.pswd().c_str(), user1.type(),
user1.phone(0).c_str());
puts(json);
printf("phone_size:%d\n", user1.phone_size());
printf("json bytes:%d\n", strlen(json));
return 0;
}
编译:g++ -g -Wall main.cpp rs/UserInfo.pb.cc -o test -lprotobuf
运行结果:
id: 1
name: "hw"
pswd: "123456"
type: ADMIN
phone: "13012345678"
protobuf bytes:29
id: 1
name: "hw"
pswd: "123456"
type: ADMIN
phone: "13012345678"
{id:1,name:hw,pswd:123456,type:1,phone:[13012345678]}
phone_size:1
json bytes:53
.proto文件
.proto文件是protobuf一个重要的文件,它定义了需要序列化数据的结构。使用protobuf的3个步骤是:
1 在.proto文件中定义消息格式
2 用protobuf编译器编译.proto文件
3 用C++/Java等对应的protobuf API来写或者读消息
程序示例(C++版)
该程序示例的大致功能是,定义一个Persion结构体和存放Persion的AddressBook,然后一个写程序向一个文件写入该结构体信息,另一个程序从文件中读出该信息并打印到输出中。
1 address.proto文件
package tutorial;
message Persion {
required string name = 1;
required int32 age = 2;
}
message AddressBook {
repeated Persion persion = 1;
}
编译.proto文件,执行命令: protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto,示例中执行命令protoc --cpp_out=/tmp addressbook.proto ,会在/tmp中生成文件addressbook.pb.h和addressbook.pb.cc。
2 write.cpp文件,向文件中写入AddressBook信息,该文件是二进制的
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
void PromptForAddress(tutorial::Persion *persion) {
cout << "Enter persion name:" << endl;
string name;
cin >> name;
persion->set_name(name);
int age;
cin >> age;
persion->set_age(age);
}
int main(int argc, char **argv) {
//GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOL_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
}
else if (!address_book.ParseFromIstream(&input)) {
cerr << "Filed to parse address book." << endl;
return -1;
}
}
// Add an address
PromptForAddress(address_book.add_persion());
{
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
//google::protobuf::ShutdownProtobufLibrary();
return 0;
}
编译write.cpp文件,g++ addressbook.pb.cc write.cpp -o write `pkg-config --cflags --libs protobuf` (注意,这里的`符号在键盘数字1键左边,也就是和~是同一个按键)。
3 read.cpp文件,从文件中读出AddressBook信息并打印
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.persion_size(); i++) {
const tutorial::Persion& persion = address_book.persion(i);
cout << persion.name() << " " << persion.age() << endl;
}
}
int main(int argc, char **argv) {
//GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOL_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Filed to parse address book." << endl;
return -1;
}
input.close();
}
ListPeople(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
//google::protobuf::ShutdownProtobufLibrary();
return 0;
}
编译read.cpp文件,g++ addressbook.pb.cc read.cpp -o read `pkg-config --cflags --libs protobuf`
4 执行程序
Protobuf3
- 设计协议是在fileName.proto文件中, 其中fileName是自己定义, 在通过protoc转换成为对应的代码。
- 关键字:
- message 表示一个消息体, 相当于一个类。
- 每个message中都有对应的变量, 每个变量有个对应的编号, 每个message内不同变量的编号不能重复。
- 新版本的protobuf没有了required, optional等说明关键字, 都默认为optional
- 基本的语法规则跟C++类似。 只不过多了后面变量的编号
\\----------------第一个文件-------------------
syntax = "proto3"; //指定版本,必须要写(proto3、proto2)
message Header{
//参数类型 参数名称=字段编码值
string identify=1;// 协议标志
string version=2;// 协议版本
string appKey=3;// appKey
int32 msgType=4;// 消息类型
int32 direction=5;// 消息方向
int64 msgTime=6;//消息时间
string ack=7;//ack
}
\\----------------第二个文件-------------------
syntax = "proto3"; //指定版本,必须要写(proto3、proto2)
//message是固定的。UserInfo是类名,可以随意指定,符合规范即可
message User{
//参数类型 参数名称=字段编码值
string senderId=1;// 发送人ID
string receiverId=2;// 接受人ID
string senderName=3;// 发送人姓名
string receiverName=4;// 接收人姓名
string senderHeadPortrait=5;// 发送人头像URL
}
//-------------第三个文件-------------
syntax = "proto3"; //指定版本,必须要写(proto3、proto2)
message Body{
//参数类型 参数名称=字段编码值
string content=1;// 内容
int32 contentLength=2;// 内容长度
int32 contentType=3;// 内容类型
int32 msgType=4;// 消息类型
}
代码生成
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
参数说明:
- IMPORT_PATH:指定 proto 文件的路径,如果没有指定, protoc 会从当前目录搜索对应的 proto 文件,如果有多个路径,那么可以指定多次
--proto_path
- 指定各语言代码的输出路径
- –cpp_out:生成c++代码
- java_out :生成java代码
- python_out :生成python代码
- go_out :生成go代码
- ruby_out :生成ruby代码
- javanano_out :适合运行在有资源限制的平台(如Android)的java代码
- objc_out :生成 Objective-C代码
- csharp_out :生成C#代码
- php_out :生成PHP代码
//指定版本 使用protobuf3
syntax = "proto3";
message Account {
//账号
uint64 ID = 1;
//名字
string name = 2;
//密码
string password = 3;
}
protoc --cpp_out=./ project.proto
protoc
是工具名, 可以直接运行的命令。
--cpp_out
是参数, 指定生成C++代码, =后面指定生成的目录。
project.proto
是定义的文件。
一共会生成两个文件。 project.pb.h
和 project.pb.cc
其中有生成的Account类中有这几个设置属性的方法
void clear_name();
static const int kNameFieldNumber = 2;
const std::string& name() const;
void set_name(const std::string& value);
void set_name(std::string&& value);
void set_name(const char* value);
void set_name(const char* value, size_t size);
std::string* mutable_name();
std::string* release_name();
void set_allocated_name(std::string* name);
// string password = 3;
void clear_password();
static const int kPasswordFieldNumber = 3;
const std::string& password() const;
void set_password(const std::string& value);
void set_password(std::string&& value);
void set_password(const char* value);
void set_password(const char* value, size_t size);
std::string* mutable_password();
std::string* release_password();
void set_allocated_password(std::string* password);
// uint64 ID = 1;
void clear_id();
static const int kIDFieldNumber = 1;
::PROTOBUF_NAMESPACE_ID::uint64 id() const;
void set_id(::PROTOBUF_NAMESPACE_ID::uint64 value);
- 使用该代码
- 在要使用的代码中包含此头文件
project.pb.h
直接使用该对象即可。编译的时候需要把project.pb.cc
编译, 并在链接的时候链接动态链接库libprotobuf.so
- demo
#include <iostream>
#include <string>
#include "project.pb.h"
int main()
{
Account account;
account.set_id(1000);
account.set_name("name");
account.set_password("password");
//序列化
std::string s = account.SerializeAsString();
if(s.size() == 0) {
std::cout << "error in SerializeAsString" << std::endl;
}
Account nAccount;
//反序列化
if(nAccount.ParseFromString(s)) {
std::cout << nAccount.id() << std::endl;
std::cout << nAccount.name() << std::endl;
std::cout << nAccount.password() << std::endl;
} else {
std::cout << "error in ParseFromString" << std::endl;
}
return 0;
}
编译
project.pb.cc
也需要编译, 如果是工程中, 可以单独编译, 由于只有一个文件所以就放到一块编译了g++ demo.cpp project.pb.cc -lprotobuf -o main
输出结果为:
1000
name
password