您的位置:首页 > Web前端

Protocol Buffer使用

2016-05-09 14:10 302 查看

Protocol Buffer使用简介

字数2630 阅读5067 评论1 喜欢12

我们项目中使用protocol buffer来进行服务器和客户端的消息交互,服务器使用C++,所以本文主要描述protocol buffer C++方面的使用,其他语言方面的使用参见google的官方文档.

1.概览

1.1 什么是protocol buffer

protocol buffer是google的一个开源项目,它是用于结构化数据串行化的灵活、高效、自动的方法,例如XML,不过它比xml更小、更快、也更简单。你可 以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。

2.使用

2.1定义一个消息类型

message SearchRequest
{
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}

该消息定义了三个字段,两个int32类型和一个string类型的字段,每个字段由字段限制,字段类型,字段名和Tag四部分组成.对于C++,每一个
.proto
文件经过编译之后都会对应的生成一个
.h
和一个
.cc
文件.

字段限制

字段限制共有3类:
required
:必须赋值的字段
optional
:可有可无的字段
repeated
:可重复字段(变长字段),类似于数值
由于一些历史原因,
repeated
字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:
repeated int32 samples = 4 [packed=true];

Tags

消息中的每一个字段都有一个独一无二的数值类型的Tag.1到15使用一个字节编码,16到2047使用2个字节编码,所以应该将Tags 1到15留给频繁使用的字段.
可以指定的最小的Tag为$$1$$,最大为$$2^{29}-1$$或$$536,870,911$$.但是不能使用$$19000$$到$$19999$$之间的值,这些值是预留给protocol buffer的.

注释

使用C/C++的
//
语法来添加字段注释.

2.2 值类型

proto的值类型与具体语言中值类型的对应关系.

2.3 可选字段与缺省值

在消息解析时,如果发现消息中没有包含可选字段,此时会将消息解析对象中相对应的字段设置为默认值,可以通过下面的语法为
optional
字段设置默认值:
optional int32 result_per_page = 3 [default = 10];

如果没有指定默认值,则会使用系统默认值,对于
string
默认值为空字符串,对于
bool
默认值为false,对于
数值类型
默认值为0,对于
enum
默认值为定义中的第一个元素.

2.4 枚举

message SearchRequest
{
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus
{
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}

由于枚举值采用varint编码,所以为了提高效率,不建议枚举值取负数.这些枚举值可以在其他消息定义中重复使用.

2.5 使用其他消息类型

可以使用一个消息的定义作为另一个消息的字段类型.
message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}

message SearchResponse
{
repeated Result result = 1;
}

可以使用
import
语法来包含另外一个
.proto
文件.
import "myproject/other_protos.proto";

2.6 嵌套类型

在protocol中可以定义如下的嵌套类型
message SearchResponse
{
message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}

如果在另外一个消息中需要使用
Result
定义,则可以通过
Parent.Type
来使用.
message SomeOtherMessage
{
optional SearchResponse.Result result = 1;
}

protocol支持更深层次的嵌套和分组嵌套,但是为了结构清晰起见,不建议使用过深层次的嵌套,建议通过 2.5 小节提到的方法来实现.

2.7 更新一个数据类型

在更新一个数据类型时更多的是需要考虑与旧版本的兼容性问题:不要改变任何已存在字段的Tag值,如果改变Tag值可能会导致数值类型不匹配,具体原因参加protocol编码建议使用
optional
repeated
字段限制,尽可能的减少
required
的使用.不需要的字段可以删除,删除字段的Tag不应该在新的消息定义中使用.不需要的字段可以转换为扩展,反之亦然只要类型和数值依然保留
int32
,
uint32
,
int64
,
uint64
, 和
bool
是相互兼容的,这意味着可以将其中一种类型任意改编为另外一种类型而不会产生任何问题
sint32
sint64
是相互兼容的
string
bytes
是相互兼容的
fixed32
兼容
sfixed32
,
fixed64
兼容
sfixed64
.
optional
兼容
repeated

2.8 扩展

extend
特性来让你声明一些Tags值来供第三方扩展使用.
message Foo
{
// ...
extensions 100 to 199;
}

假如你在你的
proto
文件中定义了上述消息,之后别人在他的
.proto
文件中import你的
.proto
文件,就可以使用你指定的Tag范围的值.
extend Foo
{
optional int32 bar = 126;
}

在访问extend中定义的字段和,使用的接口和一般定义的有点不一样,例如set方法:
Foo foo;
foo.SetExtension(bar, 15);

类似的有
HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()
等接口.

2.9 选项

optimize_for (file option): 可以设置的值有
SPEED
,
CODE_SIZE
, 或
LITE_RUNTIME
. 不同的选项会以下述方式影响C++, Java代码的生成.TSPEED (default): protocol buffer编译器将会生成序列化,语法分析和其他高效操作消息类型的方式.这也是最高的优化选项.确定是生成的代码比较大.CODE_SIZE: protocol buffer编译器将会生成最小的类,确定是比SPEED运行要慢LITE_RUNTIME: protocol buffer编译器将会生成只依赖"lite" runtime library (libprotobuf-lite instead of libprotobuf)的类. lite运行时库比整个库更小但是删除了例如descriptors 和 reflection等特性. 这个选项通常用于手机平台的优化.
option optimize_for = CODE_SIZE;

3.常用API介绍

对于如下消息定义:
// test.proto
message PBStudent
{
optional uint32 StudentID   = 1;
optional string Name        = 2;
optional uint32 Score       = 3;
}

message PBMathScore
{
optional uint32 ClassID     = 1;
repeated PBStudent ScoreInf   = 2;
}

protocol buffer编译器会为每个消息生成一个类,每个类包含基本函数,消息实现,嵌套类型,访问器等部分.

3.1 基本函数

public:
PBStudent();
virtual ~PBStudent();

PBStudent(const PBStudent& from);

inline PBStudent& operator=(const PBStudent& from) {
CopyFrom(from);
return *this;
}

inline const ::google::protobuf::UnknownFieldSet& unknown_fields() const {
return _unknown_fields_;
}

inline ::google::protobuf::UnknownFieldSet* mutable_unknown_fields() {
return &_unknown_fields_;
}

static const ::google::protobuf::Descriptor* descriptor();
static const PBStudent& default_instance();

void Swap(PBStudent* other);

3.2 消息实现

PBStudent* New() const;
void CopyFrom(const ::google::protobuf::Message& from);
void MergeFrom(const ::google::protobuf::Message& from);
void CopyFrom(const PBStudent& from);
void MergeFrom(const PBStudent& from);
void Clear();
bool IsInitialized() const;

int ByteSize() const;
bool MergePartialFromCodedStream(
::google::protobuf::io::CodedInputStream* input);
void SerializeWithCachedSizes(
::google::protobuf::io::CodedOutputStream* output) const;
::google::protobuf::uint8* SerializeWithCachedSizesToArray(::google::protobuf::uint8* output) const;
int GetCachedSize() const { return _cached_size_; }
private:
void SharedCtor();
void SharedDtor();
void SetCachedSize(int size) const;

3.3 嵌套类型

3.4 访问器

// optional uint32 StudentID = 1;
inline bool has_studentid() const;
inline void clear_studentid();
static const int kStudentIDFieldNumber = 1;
inline ::google::protobuf::uint32 studentid() const;
inline void set_studentid(::google::protobuf::uint32 value);

// optional string Name = 2;
inline bool has_name() const;
inline void clear_name();
static const int kNameFieldNumber = 2;
inline const ::std::string& name() const;
inline void set_name(const ::std::string& value);
inline void set_name(const char* value);
inline void set_name(const char* value, size_t size);
inline ::std::string* mutable_name();
inline ::std::string* release_name();
inline void set_allocated_name(::std::string* name);

// optional uint32 Score = 3;
inline bool has_score() const;
inline void clear_score();
static const int kScoreFieldNumber = 3;
inline ::google::protobuf::uint32 score() const;
inline void set_score(::google::protobuf::uint32 value);

protocol buffer编译器会对每一个字段生成一些
get
set
方法,这些方法的名称采用标识符所有小写加上相应的前缀或后缀组成.生成一个值为Tags的
k标识符FieldNum
常量,

3.5 其他函数

除了生成上述类型的方法外, 编译器还会生成一些用于消息类型处理的私有方法. 每一个
.proto
文件在编译的时候都会自动包含message.h文件,这个文件声明了很多序列化和反序列化,调试, 复制合并等相关的方法.

3.6 使用例子

在我们平时的使用中,通常一个message对应一个类,在对应的类中定义一个set和create方法来生成和解析PB信息.针对上述消息定义如下类:
// test.h
class CStudent
{
public:
unsigned    mStudentID;
unsigned    mScore;
string      mName;

CStudent()
{
Init();
}

inline void Init()
{
mStudentID = 0;
mScore = 0;
mName = "";
}
}

class CMathScore
{
private:
unsigned    mClassID;
CStudent    mScoreInf[100];
public:
CMathSCore()
{
Init();
}
~CMathScore() {};

void Init();
void SetFromPB(const PBMathScore* pPB);
void CreatePB(PBMathScore* pPB);

// Get & Set mClassID
...
// Get & set mScoreInf
...
// some other function
...
}

对应的
cpp
文件中实现对PB的操作
// test.cpp
void CMathScore::Init()
{
mClassID = 0;
memset(mScoreInf, 0, sizeof(mScoreInf));
}

void CMathScore::SetFromPB(const PBMathScore* pPB)
{
if ( NULL == pPB ) return;

mClassID = pPB->classid();
for(unsigned i = 0; i < (unsigned)pPB->scoreinf_size() && i < 100; ++i)
{
PBStudent* pStu = pPB->mutable_scoreinf(i);
mScoreInf[i].mStudentID = pStu->studentid();
mScoreInf[i].mScore        = pStu->score();
mScoreInf[i].mName        = pStu->name();
}
}

void CMathScore::CreatePB(PBMathScore* pPB)
{
if ( NULL == pPB ) return;

pPB->set_classid(mClassID);
for(unsigned i = 0; i < 100; ++i)
{
PBStudent* pStu = pPB->add_scoreinf();
pStu->set_studentid(mScoreInf[i].mStudentID)
pStu->set_score(mScoreInf[i].mScore);
pStu->set_name(mScoreInf[i].mName);
}
}

PB文件的读写
// use.cpp
#include<test.h>

#defind        MAX_BUFFER        1024 * 1024
int write()
{
CMathScore    mMath;
PBMathScore mPBMath;
// use set functions to init member variable

fstream fstm("./math.dat", ios::out | ios::binary);
if ( fstm.is_open() == false )
{
return -1;
}
char* tpBuffer = (char*)malloc(MAX_BUFFER);
if ( NULL == tpBuffer )
{
return -2;
}

mMath.CreatePB(&mPBMath);
if ( mPBMath.SerializeToArray(tpBuffer, mPBMath.ByteSize()) == false )
{
return -3;
}
fstm.write(tpBuffer, mPBMath.ByteSize());
free(tpBuffer);
fstm.close();

return 0;
}

int read()
{
CMathScore    mMath;
PBMathScore mPBMath;

fstream fstm.open("./math.dat", ios::out | ios::binary);
if ( fstm.is_open() == false )
{
return -1;
}
char* tpBuffer = (char*)malloc(MAX_BUFFER);
if ( NULL == tpBuffer )
{
return -2;
}
char*    tpIdx = tpBuffer;
int     tLen;
while ( !fstm.eof() && tLen < MAX_BUFFER )
{
fstm.read(tpIdx, 1);
tpIdx += 1;
tLen++;
}
if ( mPBMath.ParseFromArray(tpBuffer, tLen - 1) == false )
{
return -3;
}
fstm.close();
free(tpBuffer);
tpIdx = NULL;

mMath.SetFromPB(&mPBMath);
// do some thing

return 0;
}

1.下载安装:google protocol buffer 的官网地址是:http://code.google.com/p/protobuf/建议下载稳定版本:protobuf-2.4.1 linux下载protobuf-2.4.1.tar.bz2 windows下载protobuf-2.4.1.zip这里以linux下安装为实例:tar -xvf protobuf-2.4.1.tar.bz2cd protobuf-2.4.1./configure --prefix=/usr/local/protobuf-2.4.1makemake install2.使用protobuf查看编译生成的目录cd /usr/local/protobuf-2.4.1lsbin include lib其中,bin中的protoc是.proto文件的处理器,可用这个工具生成cpp,Java,Python文件.由于系统常用这个工具,可以将其ln或者直接拷贝到系统环境bin下ln -s /usr/local/protobuf-2.4.1/bin/protoc /usr/bin/protoc同样,可以将头文件ln或者直接拷贝到系统环境ln -s /usr/local/protobuf-2.4.1/include/google /usr/include/google将lib文件ln或者直接拷贝到系统环境略,方法同上.这个时候,protobuf的开发环境已经搭建了.3.如何使用protobuf[cpp] view plain copy数据结构体:message message_name{message_body;}message_body格式:例如required int32 query = 1[defaut=10];形式为:rule type name = value[other_rule];规则:required表示必须具有该值域;optional表示可选的值域;repeated表示可重复的值域(即>=0);其中requered/optional是常用rule,而repeated则不常用同时因为是历史遗留现使用repeated int32 samples=4[packed=true];形式;value值:value值最小为1,是底层编码时使用其中1-15占一位,>15则会占多位;不同的message中的value值互不干扰,常以1开始计数。数据类型之基本类型:.proto Type C++ Type Java Typedouble double doublefloat float floatint32 int32 intint64 int64 longuint32 uint32 intuint64 uint64 longsint32 int32 intsint64 int64 longfixed32 uint32 intfixed64 uint64 longsfixed32 int32 intsfixed64 int64 longbool bool booleanstring string Stringbytes string ByteString数据类型之复杂类型:复杂类型主要包括:枚举,其他message,groups等。枚举定义例如:enum Corpus{WEB=0;LOCAL=1}枚举定义在message中。可以使用其他message作为类型来定义成员。groups我的理解有些像C++中的union结构。嵌套定义:可以嵌套定义message结构,而嵌套定义的message被其他message作为成员类型时需要形式为outmessage.inmessage形式。包结构:定义形式:package foo.bar;对应C++中则生成两个命名空间foo和bar,且bar定义在foo中;可以通过import "myproject/other_protos.proto";来引入.proto文件;引用其他package中message时需要完整的package路径;Services:主要用于RPC系统中,在.proto中定义接口;定义形式如例子:service SearchService {rpc Search(SearchRequest) return (SearchResponse);}.proto文件编译:格式:protoc -–proto_path=(.proto文件路径) -–cpp_out=(.cc .java生成文件路径) (.proto文件路径)/?.proto-–proto_path 简化为: --I其中可根据需要更改:cpp_out选项为java_out/python_out。例子:protoc -I=./ --cpp_out=./ model.proto

我们拿个例子:建立model.proto[cpp] view plain copypackage cn.vicky.model.seri;message User {required int32 id = 1; // 主键,唯一required string username = 2; // 帐号required string password = 3; // 密码optional string email = 4; // 邮箱(可选)repeated Person person = 5; // 账户拥有的角色(可以重复)}message Person {required int32 id = 1; // 主键,唯一required string name = 2; // 角色名字repeated PhoneNumber phone = 3; // 电话号码(可以重复)}// 枚举类型enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {required string number = 1;optional PhoneType type = 2 [default = HOME];}
protoc -I=./ --cpp_out=./ model.proto

将生成对应的model.pb.h model.pb.cc使用:编写main.cpp[cpp] view plain copy/** File: main.cpp* Author: Vicky.H* Email: eclipser@163.com*/#include <iostream>#include <fstream>#include "model.pb.h"/***/int main(void) {// 创建User对象cn::vicky::model::seri::User u;u.set_id(1);u.set_username("Jack");u.set_password("123456");u.set_email("289997171@qq.com");// 创建User中的一个角色cn::vicky::model::seri::Person* _person1 = u.add_person();_person1->set_id(1);_person1->set_name("P1");// 创建角色中的一个电话号码:1cn::vicky::model::seri::PhoneNumber* _phone1 = _person1->add_phone();_phone1->set_number("+8613618074943");_phone1->set_type(cn::vicky::model::seri::MOBILE);// 创建角色中的一个电话号码:2cn::vicky::model::seri::PhoneNumber* _phone2 = _person1->add_phone();_phone2->set_number("02882334717");_phone2->set_type(cn::vicky::model::seri::WORK);// 创建User中的一个角色cn::vicky::model::seri::Person* _person2 = u.add_person();_person2->set_id(2);_person2->set_name("P2");// 创建角色中的一个电话号码:1cn::vicky::model::seri::PhoneNumber* _phone3 = _person2->add_phone();_phone3->set_number("+8613996398667");_phone3->set_type(cn::vicky::model::seri::MOBILE);// 创建角色中的一个电话号码:2cn::vicky::model::seri::PhoneNumber* _phone4 = _person2->add_phone();_phone4->set_number("02882334717");_phone4->set_type(cn::vicky::model::seri::WORK);// 持久化:// std::fstream out("User.pb", std::ios::out | std::ios::binary | std::ios::trunc);// u.SerializeToOstream(&out);// out.close();// 对象化:cn::vicky::model::seri::User u2;std::fstream in("User.pb", std::ios::in | std::ios::binary);if (!u2.ParseFromIstream(&in)) {std::cerr << "Failed to parse User.pb." << std::endl;exit(1);}std::cout << u2.id() << std::endl;std::cout << u2.username() << std::endl;std::cout << u2.password() << std::endl;std::cout << u2.email() << std::endl;std::cout << "---------------------------" << std::endl;for(int i = 0;i < u2.person_size();i++) {cn::vicky::model::seri::Person* p = u2.mutable_person(i);std::cout << p->id() << std::endl;std::cout << p->name() << std::endl;for (int j = 0;j < p->phone_size();j++) {cn::vicky::model::seri::PhoneNumber* phone = p->mutable_phone(j);std::cout << phone->number() << std::endl;}std::cout << "---------------------------" << std::endl;}return 0;}需要 -lpthread -lprotobuf (protobuf已经被加载到了/usr/lib)执行后,会生成:User.pb,存储的二进制文件.可以直接打开看看.以上,我们使用了protobuf完成c++下的对象序列化以及反序列化.这里我们要描述一下protobuf的优势了.那就是protobuf性能高效,他的序列化速度比java自身的序列化还快数倍,而且支持3种语言对象的转换.以往,在C++中序列化的对象,比
如用boost
serialization持久化的对象,无法用java展开,即便使用jni技术,这也是非常麻烦的事.现在我们有protobuf了.运行: protoc -I=./ --java_out=./ model.proto 将生成对应的Java类我们可以用Maven建立一个Java工程.需要protobuf的java依赖库:[html] view plain copy<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion><groupId>cn.vicky</groupId><artifactId>google_protobuf_01_java</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><name>google_protobuf_01_java</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>2.4.1</version></dependency></dependencies></project>
编写Test.java
[java] view plain copypackage cn.vicky.model.seri;import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;/**** @author Vicky.H*/public class Test {public static void main(String args[]) throws FileNotFoundException, IOException {File file = new File("User.pb");InputStream is = new FileInputStream(file);Model.User user = Model.User.parseFrom(is);System.out.println(user.getId());System.out.println(user.getUsername());System.out.println(user.getPassword());System.out.println(user.getEmail());System.out.println("-------------------");for (Model.Person person : user.getPersonList()) {System.out.println(person.getId());System.out.println(person.getName());for (Model.PhoneNumber phone : person.getPhoneList()) {System.out.println(phone.getNumber());}System.out.println("-------------------");}is.close();}}

运行:1Jack123456289997171@qq.com---------------------------1P1+861361807494302882334717---------------------------2P2+861399639866702882334717---------------------------运行 SUCCESSFUL (总时间: 594ms)OK.以上我们完成了probuf在C++,Java的使用.非常强力是不是!!设计思想:在POJO中,protobuf生成的类,处于PO状态,而且这个生成的类,我们最好不要做任何修改或太大的修改,那么,这个时候,我们可以通过C++友元类的方式,为PO添加一个JO类.将数据结构算法分离,也就是说,PO是数据,JO放算法!!!与数据库的结合:MySQL oracle 可以很轻松的存储,读取二进制.还有一点,那就是通过这种方式,我们可以非常简单的将C++的对象,持久化的redis之类内存数据库了.附:model.proto也可以这样定义,不过,本人认为,上面的更好,这里仅供参考,采用什么样的方式,生成的类的结构也不太一样.[cpp] view plain copypackage cn.vicky.model.seri;message User {required int32 id = 1; // 主键,唯一required string username = 2; // 帐号required string password = 3; // 密码optional string email = 4; // 邮箱(可选)message Person {required int32 id = 1; // 主键,唯一required string name = 2; // 角色名字// 枚举类型enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;}message PhoneNumber {required string number = 1;optional PhoneType type = 2 [default = HOME];}repeated PhoneNumber phone = 3; // 电话号码(可以重复)}repeated Person person = 5; // 账户拥有的角色(可以重复)}

                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: