我们项目中使用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.
}`</pre>
该消息定义了三个字段,两个int32类型和一个string类型的字段,每个字段由字段限制,字段类型,字段名和Tag四部分组成.对于C++,每一个`.proto`文件经过编译之后都会对应的生成一个`.h`和一个`.cc`文件.
### 字段限制
字段限制共有3类:
`required`:必须赋值的字段
`optional`:可有可无的字段
`repeated`:可重复字段(变长字段),类似于数值
由于一些历史原因,`repeated`字段并没有想象中那么高效,新版本中允许使用特殊的选项来获得更高效的编码:
<pre>`repeated int32 samples = 4 [packed=true];`</pre>
### 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`字段设置默认值:
<pre>`optional int32 result_per_page = 3 [default = 10];`</pre>
如果没有指定默认值,则会使用系统默认值,对于`string`默认值为空字符串,对于`bool`默认值为false,对于`数值类型`默认值为0,对于`enum`默认值为定义中的第一个元素.
## 2.4 枚举
<pre>`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];
}`</pre>
由于枚举值采用varint编码,所以为了提高效率,不建议枚举值取负数.这些枚举值可以在其他消息定义中重复使用.
## 2.5 使用其他消息类型
可以使用一个消息的定义作为另一个消息的字段类型.
<pre>`message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
message SearchResponse
{
repeated Result result = 1;
}`</pre>
可以使用`import`语法来包含另外一个`.proto`文件.
<pre>`import "myproject/other_protos.proto";`</pre>
## 2.6 嵌套类型
在protocol中可以定义如下的嵌套类型
<pre>`message SearchResponse
{
message Result
{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}`</pre>
如果在另外一个消息中需要使用`Result`定义,则可以通过`Parent.Type`来使用.
<pre>`message SomeOtherMessage
{
optional SearchResponse.Result result = 1;
}`</pre>
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);`</pre>
类似的有
HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()
等接口.2.9 选项
optimize_for (file option): 可以设置的值有
SPEED
,CODE_SIZE
, 或LITE_RUNTIME
. 不同的选项会以下述方式影响C++, Java代码的生成.T* SPEED (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);</pre> 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 ...
}
</pre> 对应的
cpp文件中实现对PB的操作 <pre>
// 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); }
}
</pre> PB文件的读写 <pre>
// 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;
}