环境:Ubantu,proto版本:proto3

protobuf的编译和安装

基于CMake工具编译和安装protobuf,CMake是一个根据CMake语法生成makefile的工具。步骤如下:

  1. 安装如下工具:sudo apt-get install autoconf automake libtool curl make
  2. 下载一个protobuf的发行版本,这里选择protobuf-3.20.3
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.3/protobuf-cpp-3.20.3.tar.gz
  1. 解压:tar -zxvf protobuf-cpp-3.20.3.tar.gz
  2. 进入protobuf项目目录下:cd protobuf-3.20.3/
  3. 生成配置文件:./autogen.sh
  4. 配置环境:./configure --prefix=xxxprefix选项指定安装的目录
  5. 编译:make -j 4
  6. 安装:make install
  7. 刷新动态库:ldconfig
  8. 检验安装是否成功:protoc,如果输出使用protoc工具的帮助信息则表示安装成功。

简介

  1. Protocol buffers是一种用于序列化结构数据的机制,序列化后的结构数一般用于网络传输、数据存储。
  2. Protocol buffers的优势
    1. 占用存储空间相比于json、xml等更少,因为它使用压缩存储
    2. 解析速度更快
    3. 独立于编程语言,Java、Js、Cpp等都可以使用
    4. 通过自动生成的类优化功能
  3. Protocol buffers如何工作的

Protocol Buffers的基础语法

  1. 字段类型
    1. 标量类型,如下表格所示
      .proto type c++ type
      double double
      int32 int32
      float float
      int64 int64
      uint32 uint32
      uint64 uint64
      sint32 int32
      sint64 int64
      fixed32 uint32
      fixed64 uint64
      bool bool
      string string
      bytes string
    2. enum:一个枚举定义示例如下
    enum Response {
        // 第一个枚举常量的值必须是0
        SUCCESS = 0;
        FAILED = 1;
    }
    
    1. 组合message类型:像其他编程语言一样,内嵌类类型的对象。
    message SearchResponse {
        repeated Result results = 1;
    }
    
    message Result {
        string url = 1;
        string title = 2;
        repeated string snippets = 3;
    }
    
  2. 在消息定义中,每个字段需要有个字段号,范围从1到536870911
  3. 字段修饰符:主要有三个,分别是optional、repeated、map
    1. optional:对于使用optional修饰的字段,如果字段的值没有设置,则序列化时不会包含该字段,反序列化获取该字段的值为默认值。
    2. repeated:对于repeated修饰的字段可以包含任意数量的值,等同于动态分配的数组
    3. map:键值对类型
    4. required:proto 3中已经不支持。
  4. 字段的默认值:
    1. string:空字符串
    2. bool:false
    3. bytes:空字节数组
    4. 数字类型如int32等:0
    5. repeated修饰的字段:默认值为一个空列表
    6. enum:默认值为枚举中第一个定义的值
  5. 导入其他proto文件的定义:import xxx.proto;
  6. 指定包说明符:一定程度上解决名字冲突,语法为package xxx。这个在cpp中等同于声明一个xxx的名称空间。
  7. 可以嵌套定义message:等同于c++中的内部类
  8. 定义服务:
    1. 先在proto文件中定义RPC服务接口
    2. 如果protocol buffer编译器生成cpp代码,则在proto文件中指定cc_generic_services选项的值为true。
    // 指明protocol buffer编译器基于RPC接口生成抽象服务代码
    option cc_generic_services = true;
    

Protocol Buffers的基本使用

1.步骤如下
  1. 参照Protocol Buffers的基础语法,在后缀名为.proto的文件中定义消息
  2. 由Protocol buffers的编译器protoc产生对应编程语言的源码(对于cpp语言,产生.h.cc文件),源码中将包含对消息操作的实用性方法
// 源目录即proto文件所在的目录
// 目标目录即用于存放生成cpp源文件的目录
protoc  --proto_path=源目录 --cpp_out=目标目录 PROTO_FILES

// 示例
./protoc  --proto_path=../Common --cpp_out=./ ../Common/*.proto
  1. 在应用中使用步骤2产生的方法。比如说获取字段的值、设置字段的值、将消息序列化到输出流、从输入流中解析消息
2.案例1
  1. 一个addressbook.proto定义如下
syntax = "proto3";
package tutorial;
 
import "google/protobuf/timestamp.proto";
// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;
  optional uint32 age = 6;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  // 消息中嵌套消息类型
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

  1. 使用protoc编译器编译proto文件,将产生addressbook.pb.h和addressbook.pb.cc文件
protoc --proto_path =./ --cpp_out=./ ./*.proto
  1. protoc编译器产生代码的常用方法
    1. getters:以字段名称作为方法名,此外对于string类型,还有一个mutable_字段名的getter
    const std::string& name() const;
    std::string* mutable_name();
    
    1. 对于optional修饰的字段,判断是否设置了字段的值
    bool has_age() const;
    
    1. setters:以set_字段名作为方法名
    void set_age(uint32_t value);
    
    1. 清空字段的值
    void clear_name();
    
    1. 标准的方法:用于检查或者操作message
    bool IsInitialized() const;
    // 从另一个消息值改变当前的消息值
    void CopyFrom(const Person& from);
    string DebugString() const;
    void Clear();
    
    1. 序列化:对于一个程序员定义的message,protoc编译器会产生对应的类,并且该类继承于Message类,Message类又继承于MessageLite类。MessageLite类中提供了很多默认方法的实现,比如说SerializeToString、ParseFromString等。
    bool SerializeToString(string* output) const;
    bool SerializeToOstream(ostream* output) const;
    
    1. 反序列化(解析)
    bool ParseFromString(const string& data);
    bool ParseFromIstream(istream* input);
    
2.案例2
  1. 一个login.proto文件定义如下:
syntax="proto3";    // 声明protobuf的版本

package network.cmd;    // 声明代码所在的包

import "common.proto";

// 指明protocol buffer编译器基于RPC接口生成抽象服务代码
option cc_generic_services = true;

// 定义登录请求消息类型
message LoginRequest {
    string name = 1;
    string password = 2;
}


// 定义登录响应的消息类型
message LoginResponse {
    ResultCode code = 1;
    bool success = 2;
}

// 定义一个RPC接口
service LoginServiceRpc {
    rpc login(LoginRequest) returns(LoginResponse);
}
  1. 使用protoc编译器编译proto文件,将产生login.pb.h和login.pb.cc文件
  2. 对于程序员定义的service,编译器产生相应的类A,类A继承自抽象基类Service。另外产生A_Stub类,这个类继承自A,它内部维护了一个RpcChannel,将所有对服务方法的调用转发到RpcChannel,因此程序员需要实现RpcChannel这个抽象接口。这其中包含的继承体系是A_Stub ->A->Service

class  RpcChannel {
 public:
  inline RpcChannel() {}
  virtual ~RpcChannel();

  // Call the given method of the remote service.
  virtual void CallMethod(const MethodDescriptor* method,
                          RpcController* controller,
                          const Message* request,
                          Message* response,
                          Closure* done) = 0;

 private:
  // delete copy constructor、operator=
  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(RpcChannel);
};

参考:https://protobuf.dev/