Protocol Buffer使用简介
我们项目中使用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代码的生成.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);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.bz2
cd protobuf-2.4.1
./configure --prefix=/usr/local/protobuf-2.4.1
make
make install
2.使用protobuf
查看编译生成的目录
cd /usr/local/protobuf-2.4.1
ls
bin 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
- 数据结构体:
- 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 Type
- double double double
- float float float
- int32 int32 int
- int64 int64 long
- uint32 uint32 int
- uint64 uint64 long
- sint32 int32 int
- sint64 int64 long
- fixed32 uint32 int
- fixed64 uint64 long
- sfixed32 int32 int
- sfixed64 int64 long
- bool bool boolean
- string string String
- bytes 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
- package 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
- /*
- * 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");
- // 创建角色中的一个电话号码:1
- cn::vicky::model::seri::PhoneNumber* _phone1 = _person1->add_phone();
- _phone1->set_number("+8613618074943");
- _phone1->set_type(cn::vicky::model::seri::MOBILE);
- // 创建角色中的一个电话号码:2
- cn::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");
- // 创建角色中的一个电话号码:1
- cn::vicky::model::seri::PhoneNumber* _phone3 = _person2->add_phone();
- _phone3->set_number("+8613996398667");
- _phone3->set_type(cn::vicky::model::seri::MOBILE);
- // 创建角色中的一个电话号码:2
- cn::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依赖库:
- <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- package 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();
- }
- }
运行:
1
Jack
123456
289997171@qq.com
---------------------------
1
P1
+8613618074943
02882334717
---------------------------
2
P2
+8613996398667
02882334717
---------------------------
运行 SUCCESSFUL (总时间: 594ms)
OK.以上我们完成了probuf在C++,Java的使用.非常强力是不是!!
设计思想:
在POJO中,protobuf生成的类,处于PO状态,而且这个生成的类,我们最好不要做任何修改或太大的修改,那么,这个时候,我们可以通过C++友元类的方式,为PO添加一个JO类.将数据结构算法分离,也就是说,PO是数据,JO放算法!!!
与数据库的结合:
MySQL oracle 可以很轻松的存储,读取二进制.还有一点,那就是通过这种方式,我们可以非常简单的将C++的对象,持久化的redis之类内存数据库了.
附:
model.proto也可以这样定义,不过,本人认为,上面的更好,这里仅供参考,采用什么样的方式,生成的类的结构也不太一样.
- package 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; // 账户拥有的角色(可以重复)
- }
转载于:https://www.cnblogs.com/oracleloyal/p/5473769.html