protocol buffer详解

为什么使用 Protobuf

我们的客户端程序是使用Java开发的,可能运行自不同的平台,如:Linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序之间进行数据通讯时存在多种方式用于设计消息格式,如:

  1. 直接传递C/C语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C程序而言就非常方便了,反之,该方式对于Java开发者而言就会非常繁琐,首先需要将接收到的数据存于ByteBuffer之中,再根据约定的字节序逐个读取每个字段,并将读取后的值再赋值给另外一个值对象中的域变量,以便于程序中其他代码逻辑的编写。
  2. 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。

protobuf关键字required、optional、repeated

  • required: 就是必须的意思,数据发送方和接收方都必须处理这个字段
  • optional: protobuf处理的时候另外加了一个bool的变量,用来标记这个optional字段是否有值,发送方在发送的时候,如果这个字段有值,那么就给bool变量标记为true,否则就标记为false,接收方在收到这个字段的同时,也会收到发送方同时发送的bool变量,拿着bool变量就知道这个字段是否有值了,这就是option的意思。
  • repeated: protobuf处理这个字段的时候,也类似optional字段,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。

protobuf 版本

protoc --version

Protobuf API

  • 每个字段都会有基本的 set_ get_ 方法
  • string类型的字段可以使用 mutable_方法来直接获得字符串的指针
  • 如果是optional 修饰的类型, 在没有对string类型赋值时也可以使用这个方法 mutable_方法,因为会帮我们自动初始化为 empty string

分配标识号

在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。

导入其他proto文件

使用关键字import导入另一个文件

syntax = "proto3";
import "protos/common.proto";
package FaceRecognition;
service FaceRecognitionService {    
	rpc upload_image (UploadRequest) returns (UploadResponse) {};    
	rpc recognition_image (RecognitionRequest) returns (RecognitionResponse) {};
}

枚举

  • 假设您希望为每个SearchRequest添加一个corpus字段,其中语料库可以是UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS或VIDEO。您可以非常简单地通过在消息定义中添加枚举来实现这一点,每个可能的值都是一个常量。

  • 枚举的第一个常量映射到0,每个枚举定义必须包含一个映射到零的常量作为第一个元素。

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

嵌套

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

定义服务

如果要将消息类型用于RPC (远程过程调用)系统,可以在.proto文件中定义RPC服务接口和协议缓冲编译器将使用您选择的语言生成服务接口代码和存根。

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

风格指南

message SongServerRequest {
  required string song_name = 1;
}

c++:

  const string& song_name() { ... }
  void set_song_name(const string& x) { ... }

java:

  public String getSongName() { ... }
  public Builder setSongName(String v) { ... }

package声明符,用来防止不同的消息类型有命名冲突

package foo.bar;
message Open { ... }

在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:

message Foo {
  ...
  required foo.bar.Open open = ``1``;
  ...
}

注释

和c/c++一样

标量数值类型

.proto TypeNotesC++ TypeJava TypePython Type[2]Go TypeRuby TypeC# TypePHP Type
double doubledoublefloatfloat64Floatdoublefloat
float floatfloatfloatfloat32Floatfloatfloat
int32使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
uint32使用变长编码uint32intint/longuint32Fixnum 或者 Bignum(根据需要)uintinteger
uint64使用变长编码uint64longint/longuint64Bignumulonginteger/string
sint32使用变长编码,这些编码在负值时比int32高效的多int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
sint64使用变长编码,有符号的整型值。编码时比通常的int64高效。int64longint/longint64Bignumlonginteger/string
fixed32总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。uint32intintuint32Fixnum 或者 Bignum(根据需要)uintinteger
fixed64总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。uint64longint/longuint64Bignumulonginteger/string
sfixed32总是4个字节int32intintint32Fixnum 或者 Bignum(根据需要)intinteger
sfixed64总是8个字节int64longint/longint64Bignumlonginteger/string
bool boolbooleanboolboolTrueClass/FalseClassboolboolean
string一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。stringStringstr/unicodestringString (UTF-8)stringstring
bytes可能包含任意顺序的字节数据。stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstring

Protobuf 使用

  • 定义消息格式文件,最好以proto作为后缀名
  • 使用Google提供的protocol buffers编译器来生成代码文件,一般为.h和.cc文件,主要是对消息格式以特定的语言方式描述
  • 使用protocol buffers库提供的API来编写应用程序

实例

1. 定义消息格式文件
syntax = "proto3";
package pt;
option optimize_for = LITE_RUNTIME;

message req_login
{
    string username = 1;
    string password = 2;
}
message obj_user_info
{
    string nickname = 1;      //昵称
    string icon        = 2;    //头像
    int64  coin        = 3;    //金币
    string location    = 4;    //所属地
}
//游戏数据统计
message obj_user_game_record
{
    string time = 1;
    int32 kill  = 2;        //击杀数
    int32 dead  = 3;        //死亡数
    int32 assist= 4;        //助攻数
}
message rsp_login
{
    enum RET {
        SUCCESS         = 0;
        ACCOUNT_NULL    = 1;    //账号不存在
        ACCOUNT_LOCK    = 2;    //账号锁定
        PASSWORD_ERROR  = 3;    //密码错误
        ERROR           = 10;
    }
    int32 ret = 1;
    obj_user_info user_info = 2;
    repeated obj_user_game_record record = 3;
}
2. 生成目标语言代码
//SRC_DIR   .proto文件存放目录
//--cpp_out  指示编译器生成C++代码,DST_DIR为生成文件存放目录
//game.proto 待编译的协议文件
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/game.proto
  • 对于我们的项目而言,整个系统相对比较封闭,不会和外部程序进行交互,与此同时,我们的客户端部分又是运行在Android平台,有鉴于此,我们考虑使用LITE版本的Protocol Buffer。这样不仅可以得到更高编码效率,而且生成代码编译后所占用的资源也会更少,至于反射所能带来的灵活性和极易扩展性,对于该项目而言完全可以忽略。由于我们在game.proto文件中定义选项optimize_for=LITE_RUNTIME,因此由该文件内生成的所有C++类的父类均为::google::protobuf::MessageLite,而非::google::protobuf::Message。MessageLite类是Message的父类,MessageLite中缺少对反射的支持,而此类功能均在Message类中提供了具体的实现。
protobuf自动生成的API
class rsp_login : public ::google::protobuf::MessageLite
{
public:
  //每一个message类都包含以下方法供你检测或操作
  void CopyFrom(const rsp_login& from);
  void MergeFrom(const rsp_login& from);
  void Clear();                              //清除所有字段内容,并置为空状态
  bool IsInitialized() const;                //检测所有required 是否初始化
  int ByteSize() const;                      //类所占字节数
  
  //整形变量只提供获取、修改、清除
  void clear_ret();
  ::google::protobuf::int32 ret() const;
  void set_ret(::google::protobuf::int32 value);

  //自定义类类型user_info
  bool has_user_info() const;
  void clear_user_info();
  const ::pt::obj_user_info& user_info() const;
  //自定义类型,并没提供set方法,而是通过mutable_接口返回user_info的指针,可根据此指针进行赋值操作
  ::pt::obj_user_info* mutable_user_info();
  //返回user_info字段指针,将所有权移交给此指针,并将user_info字段置为empty状态
  ::pt::obj_user_info* release_user_info();
  //使用set_allocated要小心,传入的参数需要显示allocate,设置后函数内部维护此指针
  void set_allocated_user_info(::pt::obj_user_info* user_info);

  //record为repeated的类数组类型
  int record_size() const;
  void clear_record();
  //根据id索引,返回记录的引用,const不可修改内容
  const ::pt::obj_user_game_record& record(int index) const;
  //根据id索引,返回记录的指针,以供查看、修改
  ::pt::obj_user_game_record* mutable_record(int index);
  //repeated类型提供add接口增加一条记录,并返回此记录的指针,以便对其赋值
  ::pt::obj_user_game_record* add_record();
  //提供mutable接口,并返回record字段的容器指针,可根据此指针遍历、修改
  ::google::protobuf::RepeatedPtrField< ::pt::obj_user_game_record >* mutable_record();
  //返回record字段的容器引用,const不可修改内容
  const ::google::protobuf::RepeatedPtrField< ::pt::obj_user_game_record >& record() const;
}
3. protobuf读和写
#include <iostream>
#include <string>
#include "game.pb.h"

int main()
{
    pt::rsp_login rsp{};
    rsp.set_ret(pt::rsp_login_RET_SUCCESS);
    auto user_info = rsp.mutable_user_info();
    user_info->set_nickname("dsw");
    user_info->set_icon("345DS55GF34D774S");
    user_info->set_coin(2000);
    user_info->set_location("zh");

    for (int i = 0; i < 5; i++) {
        auto record = rsp.add_record();
        record->set_time("2017/4/13 12:22:11");
        record->set_kill(i * 4);
        record->set_dead(i * 2);
        record->set_assist(i * 5);
    }

    std::string buff{};
    rsp.SerializeToString(&buff);
    //------------------解析----------------------
    pt::rsp_login rsp2{};
    if (!rsp2.ParseFromString(buff)) {
        std::cout << "parse error\n";
    }
    
    auto temp_user_info = rsp2.user_info();
    std::cout << "nickname:" << temp_user_info.nickname() << std::endl;
    std::cout << "coin:" << temp_user_info.coin() << std::endl;
    for (int m = 0; m < rsp2.record_size(); m++) {
        auto temp_record = rsp2.record(m);
        std::cout << "time:" << temp_record.time() << " kill:" << temp_record.kill() << " dead:" << temp_record.dead() << " assist:" << temp_record.assist() << std::endl;
    }
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×