RPC gRPC ProtoBuf之间的关系

简单介绍这几个项目的关系

  • ProtoBuf是一种序列化数据结构的协议

  • protoc是一个能把proto数据结构转换为各种语言代码的工具

  • RPC是一种通信协议

  • gRPC是一种使用ProtoBuf作为接口描述语言的一个RPC实现方案

RPC

在分布式计算,远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

RPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(常常是不兼容)都是基于这种概念而实现的。

流程如下:

客户端调用客户端stub(client stub)。这个调用是在本地,并将调用参数push到栈(stack)中。

客户端stub(client stub)将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫marshalling(如:XML、JSON、protobuf)

扫描二维码关注公众号,回复: 14255140 查看本文章

客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议或HTTP/2传输)

服务器系统将信息传送至服务端stub(server stub)。

服务端stub(server stub)解析信息。该过程叫unmarshalling。

服务端stub(server stub)调用程序,并通过类似的方式返回给客户端。在这里插入图片描述

ProtoBuf

Protocol Buffers(简称ProtoBuf)是一种序列化数据结构的协议。对于透过管道(pipeline)或存储资料进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些>描述产生代码,用于将这些数据结构产生或解析资料流。

基本类型:double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string, bytes。数字类型(double, float, int32, int64 …)默认值是0,string类型默认值是空字符,bytes默认值是空byte,bool类型的默认值是false,enum类型的默认值是枚举列表中的一个值。

字段规则:必须required, 可选optional, 重复repeated。由于历史原因repeated字段可能不能有效的编码,所以我们需要字段的后面添加一个选项[packed=true]

保留字段:reserved,由于消息更新当某些字段到xx版本可能要去掉,在去掉之前的应该使用reserved标记一下,让使用者有个过渡时间。支持标注字段编号或者字段名字。

注释风格:支持C/C++风格//和/* ... */

message定义

以下是官方文档中的例子

规范的写法是每一个字段都应该制定它的规则(required, optional, repeated)
每一个字段后面都会有一个数字(大于等于1),这个数字叫做分配字段编号(Assigning Field Numbers),它必须是一个唯一的数字,这些数字用于标识消息二进制格式的字段,一旦使用了消息类型,就不应更改这些数字。
我们可以通过[default = xx]给字段设置一个默认值

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];
}
  • 嵌套类型
message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      required int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      required int32 ival = 1;
      optional bool  booly = 2;
    }
  }
}

message扩展

message的最后可以使用extensions关键字保留的扩展字段编号,扩展的地方使用extend声明同一个消息并进行扩展。

message Foo {
  // ...
  extensions 100 to 199;
}

extend Foo {
  optional int32 bar = 126;
}
  • 嵌套扩展
    以下这种嵌套扩展bar属于Baz这个message
message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

 Services定义

ProtoBuf还支持定义RPC服务,在这个例子中SearchService是一个RPC服务,Search是服务提供的方法,这个方法的请求参数是SearchRequest,返回值是SearchResponse。

客户端:protoc会为我们生成一个SearchService接口和一个对应的stub实现。stub将所有调用转发到RpcChannel,RpcChannel是一个抽象接口,需要实现消息序列化和数据传输。在这个例子中也就是这样调用SearchService::NewStub(new MyRpcChannel())->Search。
服务端:服务端需要去实现Service接口,在这个例子中也就是继承SearchService并实现Search接口。

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

本文结尾底部,领取最新最全C++音视频学习提升资料,内容包括(C/C++Linux 服务器开发,FFmpeg webRTC rtmp hls rtsp ffplay srs

 

protoc(Protocol Compiler)

要生成Java、Python、C ++、Go、Ruby、Objective-C或C#代码,您需要使用.proto文件中定义的消息类型,需要在.proto上运行协议缓冲区编译器协议。如果尚未安装编译器,请下载软件包并按照自述文件中的说明进行操作。

$ protoc [OPTION] PROTO_FILES

  • 例子
  • $ mkdir out
    $ protoc --cpp_out=out helloworld.proto route_guide.proto
    

protoc-gen-go

protocol buffer编译器需要一个插件来生成Go代码。使用Go 1.16或更高版本通过运行以下命令安装它:

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

通过在调用protoc时传递go_opt标志来提供特定于protoc-gen-go的标志。可以传递多个go_opt标志。例如,运行时:

$ protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto

编译器将会从src目录读取文件foo.proto和bar/baz.proto文件。生成文件会存放在out目录中,生成的文件名是foo.pb.go和bar/baz.pb.go,它会主动创建向对应的子目录。

gRPC

gRPC(gRPC Remote Procedure Calls)是Google发起的一个开源远程过程调用(Remote procedure call)系统。该系统基于HTTP/2协议传输,使用Protocol Buffers作为接口描述语言。
其他功能:

  • 认证(authentication)

  • 双向流(bidirectional streaming)

  • 流控制(flow control)

  • 超时(timeouts)

可能的使用场景:

  • 内部微服务之间的通信。

  • 高数据负载(gRPC 使用协议缓冲区,其速度最高可比 REST 调用快七倍)。

  • 您只需要一个简单的服务定义,不需要编写完整的客户端库。

  • 在gRPC服务器中使用流式传输gRPC来构建响应更快的应用和 API。

编译gRPC(mac)

grpc支持的构建工具有makefile、cmake、ninja(gn)、bazel等,其中makefile不支持生成plugin,因为我这里仅仅是为了跑它的测试程序用于学习的,所以就使用cmake了

# 下载代码
$ git clone -b RELEASE_TAG_HERE https://github.com/grpc/grpc
$ cd grpc
$ git submodule update --init
# 编译
$ mkdir out && cd out
$ cmake .. -DgRPC_INSTALL=ON && make -j8 && make install

编译examples/cpp/helloworld

这里我们使用Makefile编译,直接执行编译会遇到如下编译和链接错误,这里是因为没有指定absl的搜索路径和没有链接CoreFoundation导致的

/usr/local/include/grpcpp/impl/codegen/sync.h:35:10: fatal error: 'absl/synchronization/mutex.h' file not found
#include "absl/synchronization/mutex.h"
Undefined symbols for architecture x86_64:
  "_CFRelease", referenced from:
      absl::lts_2020_09_23::time_internal::cctz::local_time_zone() in libabsl_time_zone.a(time_zone_lookup.cc.o)

修改Makefile如下:

index 8030ba2be2..de1fd6fb7f 100644
--- a/examples/cpp/helloworld/Makefile
+++ b/examples/cpp/helloworld/Makefile
@@ -17,13 +17,14 @@
 HOST_SYSTEM = $(shell uname | cut -f 1 -d_)
 SYSTEM ?= $(HOST_SYSTEM)
 CXX = g++
-CPPFLAGS += `pkg-config --cflags protobuf grpc`
+CPPFLAGS += `pkg-config --cflags protobuf grpc` -I../../../third_party/abseil-cpp
 CXXFLAGS += -std=c++11
 ifeq ($(SYSTEM),Darwin)
 LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
            -pthread\
            -lgrpc++_reflection\
-           -ldl
+           -ldl\
+                  -framework CoreFoundation -labsl_time_zone
 else
 LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
            -pthread\

编译步骤如下

$ cd examples/cpp/helloworld
$ make -j8

运行,开两个终端一个先运行服务端greeter_server,另一个运行greeter_client会打印Greeter received: Hello world

代码理解(examples/cpp/helloworld)

proto文件(helloworld.proto)

  • proto文件中option选项的java_xx字段只用于Android平台,objc_xx字段只用于Mac和iOS。使用proto会生成这几个文件helloworld.grpc.pb.cc, helloworld.grpc.pb.h, pb.cc,, helloworld.pb.h。

  • helloworld.pb.cc和 helloworld.pb.h是protoc生成的

  • helloworld.grpc.pb.cc和helloworld.grpc.pb.h是grpc_cpp_plugin生成的

  • protoc会自动生成message的实现,但是RpcChannel和Server需要我们自己实现,这里gRPC帮我们做了序列化和传输相关的工作。

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

helloworld.grpc.pb.h(cc)文件

实现了Greeter::Stub,并实现了SayHello(同步)和PrepareAsyncSayHello(异步)函数,客户端可以直接调用Greeter::Stub对象的SayHello或者PrepareAsyncSayHello方法

封装了一个Greeter::Service(同步)和Greeter::AsyncService(异步)类,服务端需要继承其中一个Service并实现里面的抽象函数SayHello就好了

namespace helloworld {
// The greeting service definition.
class Greeter final {
 public:
  static constexpr char const* service_full_name() {
    return "helloworld.Greeter";
  }
  class Stub final : public StubInterface {
   public:
    Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel){}
    ::grpc::Status SayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest& request, ::helloworld::HelloReply* response) override;
    std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::helloworld::HelloReply>> PrepareAsyncSayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest& request, ::grpc::CompletionQueue* cq) {
      return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::helloworld::HelloReply>>(PrepareAsyncSayHelloRaw(context, request, cq));
    }
    class experimental_async final : public StubInterface::experimental_async_interface {
     public:
      void SayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest* request, ::helloworld::HelloReply* response, std::function<void(::grpc::Status)>) override;
    };
    class experimental_async_interface* experimental_async() override { return &async_stub_; }
   private:
    std::shared_ptr< ::grpc::ChannelInterface> channel_;
    class experimental_async async_stub_{this};
    const ::grpc::internal::RpcMethod rpcmethod_SayHello_;
  };
    ...
  class Service : public ::grpc::Service {
   public:
    Service();
    virtual ~Service();
    // Sends a greeting
    virtual ::grpc::Status SayHello(::grpc::ServerContext* context, const ::helloworld::HelloRequest* request, ::helloworld::HelloReply* response);
  };
  template <class BaseClass>
  class WithAsyncMethod_SayHello : public BaseClass {
    // disable synchronous version of this method
    ::grpc::Status SayHello(::grpc::ServerContext* /*context*/, const ::helloworld::HelloRequest* /*request*/, ::helloworld::HelloReply* /*response*/) override {
      abort();
      return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
    }
    void RequestSayHello(::grpc::ServerContext* context, ::helloworld::HelloRequest* request, ::grpc::ServerAsyncResponseWriter< ::helloworld::HelloReply>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
      ::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
    }
  };
  typedef WithAsyncMethod_SayHello<Service > AsyncService;
  ...
} // end namespace helloworld

同步RPC例子greeter_client.cc和greeter_server.cc

  • greeter_client.cc
// 客户端例子中它进一步封装了
class GreeterClient {
 public:
  GreeterClient(std::shared_ptr<Channel> channel)  : stub_(Greeter::NewStub(channel)) {}
  std::string SayHello(const std::string& user) {
    // 要发送给服务端的数据
    HelloRequest request;
    request.set_name(user);
    // 从服务端接收到的数据
    HelloReply reply;
    // 一个客户端上下文,暂时不深究
    ClientContext context;
    // RPC操作
    Status status = stub_->SayHello(&context, request, &reply);
    // 检查状态,并返回
    if (status.ok()) {  return reply.message();  } else { return "RPC failed"; }
  }
 private:
  std::unique_ptr<Greeter::Stub> stub_;
};
  • greeter_server.cc
// 继承Greeter::Service,并重写SayHello方法
class GreeterServiceImpl final : public Greeter::Service {
  Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override {
    std::string prefix("Hello ");
    reply->set_message(prefix + request->name());
    return Status::OK;
  }
};

异步RPC例子greeter_async_client.cc和greeter_async_server.cc

  • greeter_async_client.cc
// 客户端例子中它进一步封装了
class GreeterClient {
 public:
  explicit GreeterClient(std::shared_ptr<Channel> channel) : stub_(Greeter::NewStub(channel)) {}
  std::string SayHello(const std::string& user) {
    // 要发送给服务端的数据
    HelloRequest request;
    request.set_name(user);
    // 从服务端接收到的数据
    HelloReply reply;
    // 一个客户端上下文,暂时不深究
    ClientContext context;
    // 我们用来与gRPC运行时异步通信的生产者-消费者队列
    CompletionQueue cq;
    // Storage for the status of the RPC upon completion.
    Status status;
    // 调用stub_->PrepareAsyncSayHello() 会返回一个RPC对象,暂时不深究这个对象
    std::unique_ptr<ClientAsyncResponseReader<HelloReply> > rpc(stub_->PrepareAsyncSayHello(&context, request, &cq));

    // 初始化RPC调用
    rpc->StartCall();
    // 这里是真正的执行,reply是我们期待的返回值,status是返回状态,1是一个tag(我们通过tag判断是哪个调用返回的值)
    rpc->Finish(&reply, &status, (void*)1);
    void* got_tag;
    bool ok = false;
    // 会一直阻塞等待rpc返回,我们通过tag区分是哪一个调用
    GPR_ASSERT(cq.Next(&got_tag, &ok));
    // 在这个例子中我们只有一个调用,这个tag是1,这个这个tag一定是1,如果不是1就直接assert掉
    GPR_ASSERT(got_tag == (void*)1);
    // 检查状态,并返回
    if (status.ok()) {  return reply.message();  } else { return "RPC failed"; }
  }
 private:
  std::unique_ptr<Greeter::Stub> stub_;
};

greeter_async_server.cc
它这里也是做了一层封装,不是很好理解,因为它递归调用自己的次数比较多,实际上接口很简答的。
调用Server的RequestSayHello把Callback加入ServerCompletionQueue中
然后调用ServerCompletionQueue的Next(cq_->Next(&tag, &ok))接口等待客户端请求消息
当我们收到客户端请求的时候就会走到下一步,我们调用Callback对象的Proceed()接口去处理客户端请求,此时Callback对象的状态变为PROCESS,所以它会走PROCESS分支,服务端处理完之后调用responder_.Finish返回处理后的结果给到客户端。同时在一开始的时候我们又会创建一个新的Callback加入等待队列。
在调用responder_.Finish之前我们需要把状态修改为FINISH,因为responder_.Finish也是异步的,所以此时我们又阻塞在cq_->Next(&tag, &ok)位置,等待responder_.Finish执行结束再次唤醒我们。
此时我们又接着执行把Callback对象的Proceed()接口,这次我们执行FINISH分支把自己给释放掉。

class ServerImpl final {
 public:
  ~ServerImpl() {  server_->Shutdown();  cq_->Shutdown();  }
  // This can be run in multiple threads if needed.
  void Run() {
    new CallData(&service_, cq_.get());
    void* tag;  // uniquely identifies a request.
    bool ok;
    while (true) {
      GPR_ASSERT(cq_->Next(&tag, &ok));
      GPR_ASSERT(ok);
      static_cast<CallData*>(tag)->Proceed();
    }
  }

 private:
  // Class encompasing the state and logic needed to serve a request.
  class CallData {
   public:
    CallData(Greeter::AsyncService* service, ServerCompletionQueue* cq) : service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
      Proceed();
    }

    void Proceed() {
      if (status_ == CREATE) {
        status_ = PROCESS;
        service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_, this);
      } else if (status_ == PROCESS) {
        new CallData(service_, cq_);
        std::string prefix("Hello ");
        reply_.set_message(prefix + request_.name());
        status_ = FINISH;
        responder_.Finish(reply_, Status::OK, this);
      } else {
        GPR_ASSERT(status_ == FINISH);
        delete this;
      }
    }

   private:
    Greeter::AsyncService* service_;
    ServerCompletionQueue* cq_;
    ServerContext ctx_;
    HelloRequest request_;
    HelloReply reply_;
    ServerAsyncResponseWriter<HelloReply> responder_;
    enum CallStatus { CREATE, PROCESS, FINISH };
    CallStatus status_;  // The current serving state.
  };

  std::unique_ptr<ServerCompletionQueue> cq_;
  Greeter::AsyncService service_;
  std::unique_ptr<Server> server_;
};

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/125224381