Bootstrap

详解 Protobuf 在 C++ 下 Message、enum、Service 的使用

这篇文章主要目的是介绍Protobuf的常用知识,包括前置声明,message,service,enum 等。

声明

// 使用 proto3 语法
syntax = "proto3";

// 定义一个名为 Greeter 的包
package Greeter;

// 开启生成通用服务代码的选项
option cc_generic_services = true;

syntax 用于提示 protoc 使用哪个版本的 Protobuf(proto2 || proto3),package 定义的包会在 protoc 编译之后被翻译为namespace,就如上面示例中的 Greeter 会被翻译为 namespace Greeter{};。cc_generic_services 设置为 true 用于提示 protoc 生产通用服务代码,简单来说就是 example 示例中需要继承的 RPC 服务类就需要 cc_generic_services 设置开启才能生成。

假设以下示例均被定义在 package Greeter 下

message

message 为消息类型,这是 protobuf 中最常用的类型。任意 message 都会在 protoc 编译后生成同名的类以及 message 内部数据对应的 setter 方法和 getter 方法(该方法和变量同名)。

示例如下:

// 定义一个名为 HelloRequest 的消息类型,其中包含一个名为 name 的字段
message HelloRequest {
    string name = 1;
}

在C++代码中使用它:

Greeter::HelloRequest* request;
request->set_name("noname");
std::string name = request->name();
std::cout << name << std::endl;

enum

enum Status {
    OK = 0;
    ERROR = 1;
}

enum 是 Protobuf 中的枚举类型,我们可以利用 enum 设计响应码,错误码等。protoc 编译后可以在 C++ 代码中以如下方式调用:

Greeter::Status::OK

在 message 中定义 Status 类型然后使用:

message HelloReply {
    string message = 1;
    Status status = 2;
}
Greeter::HelloReply* response;
response->set_status(Greeter::Status::OK);

service

service 是 protobuf 提供的用于定义 rpc 服务的关键字。我们可以使用 service 定义一个 rpc 服务,然后在该服务内定义多个 rpc 方法,每个 rpc 方法都接受一个用于请求的 message 类型的和用于 响应的 message 类型。

示例如下:

service GreetingService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

在上述示例中,我们定义了一个名为 GreetingService 的 RPC 服务,然后在 GreetingService 下定义了一个名为 SayHello 的 rpc 方法,接收 HelloRequest 类型的 message, 响应 HelloReply 类型的 message。

当使用 protoc 编译 proto 文件后 service 对应 .pb.h 中的如下内容:

class GreetingService : public ::PROTOBUF_NAMESPACE_ID::Service {
 protected:
  // This class should be treated as an abstract interface.
  inline GreetingService() {};
 public:
  virtual ~GreetingService();

  typedef GreetingService_Stub Stub;

  static const ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor* descriptor();

  virtual void SayHello(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
                       const ::Greeter::HelloRequest* request,
                       ::Greeter::HelloReply* response,
                       ::google::protobuf::Closure* done);

  // implements Service ----------------------------------------------

  const ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor* GetDescriptor();
  void CallMethod(const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method,
                  ::PROTOBUF_NAMESPACE_ID::RpcController* controller,
                  const ::PROTOBUF_NAMESPACE_ID::Message* request,
                  ::PROTOBUF_NAMESPACE_ID::Message* response,
                  ::google::protobuf::Closure* done);
  const ::PROTOBUF_NAMESPACE_ID::Message& GetRequestPrototype(
    const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method) const;
  const ::PROTOBUF_NAMESPACE_ID::Message& GetResponsePrototype(
    const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method) const;

 private:
  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GreetingService);
};

也就是说 service 被 protoc 编译为对应的抽象类,抽象类下提供了通用的抽象接口。下面我们一一介绍这些抽象接口。

注意:抽象类中的 PROTOBUF_NAMESPACE_ID 就是 google::protobuf,所以我们覆盖虚函数的时候使用 goole::protobu 代替 PROTOBUF_NAMESPACE_ID。

构造函数

// ...
 protected:
  // This class should be treated as an abstract interface.
  inline GreetingService() {};
// ...

这里的构造函数访问权限是 protected,protected 的特点是外部不可见,内部可见且继承可见,所以将构造函数设置为 protected 也就意味着无法直接实例化类,但是构造函数能被派生类调用,也就意味着派生类继承后能正常完成实例化。所以这种做法常用于设计抽象基类,这里的 GreetingService 就是一个抽象基类。我们在实现 rpc 服务接口时,就会对这个抽象基类进行继承。

descriptor

// ...
    static const ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor* descriptor();
// ...
    const ::PROTOBUF_NAMESPACE_ID::ServiceDescriptor* GetDescriptor();

这里的 descriptor() 和 GetDescriptor() 会获取 GreetingService 服务的描述符,服务描述符包含了服务的名称、rpc方法的数量以及方法的描述符等。而方法的描述符则包含了方法的名称、方法的输入输出描述符。descriptor() 和 GetDescriptor() 的区别在于它们的调用方式不同。descriptor() 是静态方法,可以在不创建实例的情况下调用。而GetDescriptor() 是非静态方法,需要先创建类的实例再进行调用。

使用示例:

// 获取 GreetingService 服务的描述
const ::google::protobuf::ServiceDescriptor* service_desc 
    = Greeter::GreetingService::descriptor();

// 获取服务的名称
std::string service_name = service_desc->name();

// 获取服务中的方法数量
int methodCnt = service_desc->method_count();

// 遍历每个方法
for(int i = 0; i < methodCnt; i++) {
    // 获取方法的描述
    const google::protobuf::MethodDescriptor *pmethodDesc =
        service_desc->method(i);
    
    // 获取方法的名称
    std::string method_name = pmethodDesc->name();

    // 获取方法的输入描述
    const google::protobuf::Descriptor *input_type = pmethodDesc->input_type();
    // 获取方法的输入类型
    std::string input_type_name = input_type->name();

    // 获取方法的输出描述
    const google::protobuf::Descriptor *output_type = pmethodDesc->output_type();
    // 获取方法的输出类型
    std::string output_type_name = output_type->name();
}

virtual method

// ...
  virtual void SayHello(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
                       const ::Greeter::HelloRequest* request,
                       ::Greeter::HelloReply* response,
                       ::google::protobuf::Closure* done);
// ...

我们在继承了 GreetingSevice 的类中通过重写这个虚函数来完成 rpc 方法的具体实现。

class GreeterServiceImpl final : public Greeter::GreetingService {
    /**
     * SayHello 是一个 RPC 方法,用于发送问候
     *
     * @param controller: 指向 RpcController 对象的指针,用于控制 RPC 的行为。
     * @param request: 指向 HelloRequest 对象的指针,其中包含 RPC 方法的请求参数。
     * @param response: 指向 HelloReply 对象的指针,应在其中设置 RPC 方法的响应结果。
     * @param done: 指向 Closure 对象的指针,表示 RPC 方法完成后应执行的操作。
     */
    void SayHello
        (
            RpcController* controller,
            const ::Greeter::HelloRequest* request,
            ::Greeter::HelloReply* response,
            Closure* done
        ) {
            // 从请求中获取名字
            std::string name = request->name();

            // 创建一条包含名字的问候消息
            std::string message = "Hello, " + name + "!";

            // 将问候消息设置为响应的消息
            response->set_message(message);

            // 将响应的状态设置为 OK
            response->set_status(Greeter::Status::OK);

            // 运行 done 的 Run 方法,表示 RPC 方法已经完成
            done->Run();
        }
};

CallMethod

// ...
  /**
   * @brief 调用服务的方法,传入请求消息和响应消息,以及回调函数
   * 
   * @param method 方法描述符,描述了要调用的方法
   * @param controller 控制器,用于控制 RPC 调用
   * @param request 请求消息,包含了调用方法所需的参数
   * @param response 响应消息,用于存储方法的返回值
   * @param done 闭包,当 RPC 方法完成时会被调用
   */
  void CallMethod(
      const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method,
      ::PROTOBUF_NAMESPACE_ID::RpcController* controller,
      const ::PROTOBUF_NAMESPACE_ID::Message* request,
      ::PROTOBUF_NAMESPACE_ID::Message* response,
      ::google::protobuf::Closure* done
  );
// ...

CallMethod 是 RPC 服务调用的关键方法,CallMethod 会调用对应 Rpc 请求的 mehod,传递请求消息作为函数参数,并将结果封装为响应消息类型,并且调用在方法执行完毕后调用,done 定义的回调函数。

使用示例:

// 从服务对象和方法描述符中获取请求消息的原型,并创建一个新的请求消息
google::protobuf::Message *request =
    service->GetRequestPrototype(method).New();

// 从服务对象和方法描述符中获取响应消息的原型,并创建一个新的响应消息
google::protobuf::Message *response =
    service->GetResponsePrototype(method).New();

// 只需要知道上述内容是请求消息类型和响应消息类型即可,通过method方法描述符获取消息原型的示例在最下面的部分。

// 创建一个回调函数,当服务的方法调用完成后,这个回调函数会被调用
// 这个回调函数会调用 RpcProvider 的 SendRpcResponse 方法,将响应消息发送给客户端
// RpcProvider 是当前对象, TcpConnectionPtr 和 Message是需要接受的参数类型
/**
 * @param this: 指向当前对象的指针,即 RpcProvider 对象。
 * @param &RpcProvider::SendRpcResponse: 成员函数指针,指向 RpcProvider 类的 SendRpcResponse 成员函数。
 * @param conn: 一个指向 TcpConnection 的智能指针,表示一个 TCP 连接。这个参数将被传递给 SendRpcResponse 函数。
 * @param response: 指向 google::protobuf::Message 的指针,表示一个 Protobuf 消息。这个参数将被传递给 SendRpcResponse 函数。
 */
google::protobuf::Closure *done =
    google::protobuf::NewCallback<RpcProvider,
                                    const muduo::net::TcpConnectionPtr &,
                                      google::protobuf::Message *>(
          this, &RpcProvider::SendRpcResponse, conn, response);

// 调用服务的方法,传入请求消息和响应消息,以及回调函数
service->CallMethod(method, nullptr, request, response, done);
// RpcProvider::SendRpcResponse 函数定义
void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr &conn,
                                  google::protobuf::Message *response) {
// 具体实现
}

其他

// ...
  const ::PROTOBUF_NAMESPACE_ID::Message& GetRequestPrototype(
    const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method) const;
  const ::PROTOBUF_NAMESPACE_ID::Message& GetResponsePrototype(
    const ::PROTOBUF_NAMESPACE_ID::MethodDescriptor* method) const;

 private:
  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GreetingService);
// ...

GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(GreetingService) 是 protobuf 中定义的一个宏,用于删除复制构造函数和复制赋值运算符。

GetRequestPrototype 和 GetResponsePrototype的使用方式就是通过方法描述符来获取一个请求消息原型或者响应消息圆型,如下

//...
// 使用上述遍历方法获取其中的方法描述符
// 该method
const google::protobuf::MethodDescriptor *pmethodDesc =
    service_desc->method(i);

// 获取请求消息原型
google::protobuf::Message *request =
    service->GetRequestPrototype(method).New();

// 获取响应消息原型
google::protobuf::Message *response =
    service->GetResponsePrototype(method).New();

在 RpcChannel 中也存在 CallMethod 这个 CallMethod 是提供给调用方使用的,虚函数声明如下:

  virtual void CallMethod(const MethodDescriptor* method,
                          RpcController* controller, const Message* request,
                          Message* response, Closure* done) = 0;

不同于被调用方的 CallMethd 这个 CallMthod 请求和响应对象不需要是任何特定的类,只要它们的描述符是 method->input_type() 和 method->output_type() 即可。

;