gRPC官方文檔(gRPC基礎:C++)

文章來自gRPC 官方文檔中文版html

本教程提供了C++程序員如何使用gRPC的指南。git

經過學習教程中例子,你能夠學會如何:程序員

  • 在一個 .proto 文件內定義服務.
  • 用 protocol buffer 編譯器生成服務器和客戶端代碼.
  • 使用 gRPC 的 C++ API 爲你的服務實現一個簡單的客戶端和服務器.

假設你已經閱讀了概覽而且熟悉protocol buffers. 注意,教程中的例子使用的是 protocol buffers 語言的 proto3 版本,它目前只是 alpha 版:能夠在proto3 語言指南和 protocol buffers 的 Github 倉庫的版本註釋發現更多關於新版本的內容.github

這算不上是一個在 C++ 中使用 gRPC 的綜合指南:之後會有更多的參考文檔.數據庫

爲何使用 gRPC?

咱們的例子是一個簡單的路由映射的應用,它容許客戶端獲取路由特性的信息,生成路由的總結,以及交互路由信息,如服務器和其餘客戶端的流量更新。服務器

有了 gRPC, 咱們能夠一次性的在一個 .proto 文件中定義服務並使用任何支持它的語言去實現客戶端和服務器,反過來,它們能夠在各類環境中,從Google的服務器到你本身的平板電腦- gRPC 幫你解決了不一樣語言間通訊的複雜性以及環境的不一樣.使用 protocol buffers 還能得到其餘好處,包括高效的序列號,簡單的 IDL 以及容易進行接口更新。異步

例子代碼和設置

教程的代碼在這裏 grpc/grpc/examples/cpp/route_guide. 要下載例子,經過運行下面的命令去克隆grpc代碼庫:ide

$ git clone https://github.com/grpc/grpc.git

改變當前的目錄到examples/cpp/route_guide函數

$ cd examples/cpp/route_guide

你還須要安裝生成服務器和客戶端的接口代碼相關工具-若是你尚未安裝的話,查看下面的設置指南 C++快速開始指南工具

定義服務

咱們的第一步(能夠從概覽中得知)是使用 protocol buffers去定義 gRPC service 和方法 request 以及 response 的類型。你能夠在examples/protos/route_guide.proto看到完整的 .proto 文件。

要定義一個服務,你必須在你的 .proto 文件中指定 service

service RouteGuide {
   ...
}

而後在你的服務中定義 rpc 方法,指定請求的和響應類型。gRPC允 許你定義4種類型的 service 方法,在 RouteGuide 服務中都有使用:

  • 一個 簡單 RPC , 客戶端使用存根發送請求到服務器並等待響應返回,就像日常的函數調用同樣。
// Obtains the feature at a given position.
   rpc GetFeature(Point) returns (Feature) {}
  • 一個 服務器端流式 RPC , 客戶端發送請求到服務器,拿到一個流去讀取返回的消息序列。 客戶端讀取返回的流,直到裏面沒有任何消息。從例子中能夠看出,經過在 響應 類型前插入 stream 關鍵字,能夠指定一個服務器端的流方法。
// Obtains the Features available within the given Rectangle.  Results are
  // streamed rather than returned at once (e.g. in a response message with a
  // repeated field), as the rectangle may cover a large area and contain a
  // huge number of features.
  rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • 一個 客戶端流式 RPC , 客戶端寫入一個消息序列並將其發送到服務器,一樣也是使用流。一旦客戶端完成寫入消息,它等待服務器完成讀取返回它的響應。經過在 請求 類型前指定 stream 關鍵字來指定一個客戶端的流方法。
// Accepts a stream of Points on a route being traversed, returning a
  // RouteSummary when traversal is completed.
  rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • 一個 雙向流式 RPC 是雙方使用讀寫流去發送一個消息序列。兩個流獨立操做,所以客戶端和服務器能夠以任意喜歡的順序讀寫:好比, 服務器能夠在寫入響應前等待接收全部的客戶端消息,或者能夠交替的讀取和寫入消息,或者其餘讀寫的組合。 每一個流中的消息順序被預留。你能夠經過在請求和響應前加 stream 關鍵字去制定方法的類型。
// Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

咱們的 .proto 文件也包含了全部請求的 protocol buffer 消息類型定義以及在服務方法中使用的響應類型-好比,下面的Point消息類型:

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

生成客戶端和服務器端代碼

接下來咱們須要從 .proto 的服務定義中生成 gRPC 客戶端和服務器端的接口。咱們經過 protocol buffer 的編譯器 protoc 以及一個特殊的 gRPC C++ 插件來完成。

簡單起見,咱們提供一個 makefile 幫您用合適的插件,輸入,輸出去運行 protoc(若是你想本身去運行,確保你已經安裝了 protoc,而且請遵循下面的 gRPC 代碼安裝指南)來操做:

$ make route_guide.grpc.pb.cc route_guide.pb.cc

實際上運行的是:

$ protoc -I ../../protos --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../../protos/route_guide.proto
$ protoc -I ../../protos --cpp_out=. ../../protos/route_guide.proto

運行這個命令能夠在當前目錄中生成下面的文件:

  • route_guide.pb.h, 聲明生成的消息類的頭文件
  • route_guide.pb.cc, 包含消息類的實現
  • route_guide.grpc.pb.h, 聲明你生成的服務類的頭文件
  • route_guide.grpc.pb.cc, 包含服務類的實現

這些包括:

  • 全部的填充,序列化和獲取咱們請求和響應消息類型的 protocol buffer 代碼
  • 名爲 RouteGuide 的類,包含
    • 爲了客戶端去調用定義在 RouteGuide 服務的遠程接口類型(或者 存根 )
    • 讓服務器去實現的兩個抽象接口,同時包括定義在 RouteGuide 中的方法。

建立服務器

首先來看看咱們如何建立一個 RouteGuide 服務器。若是你只對建立 gRPC 客戶端感興趣,你能夠跳過這個部分,直接到建立客戶端 (固然你也可能發現它也頗有意思)。

RouteGuide 服務工做有兩個部分:

  • 實現咱們服務定義的生成的服務接口:作咱們的服務的實際的「工做」。
  • 運行一個 gRPC 服務器,監聽來自客戶端的請求並返回服務的響應。

你能夠從examples/cpp/route_guide/route_guide_server.cc看到咱們的 RouteGuide 服務器的實現代碼。如今讓咱們近距離研究它是如何工做的。

實現RouteGuide

咱們能夠看出,服務器有一個實現了生成的 RouteGuide::Service 接口的 RouteGuideImpl 類:

class RouteGuideImpl final : public RouteGuide::Service {
...
}

在這個場景下,咱們正在實現 同步 版本的RouteGuide,它提供了 gRPC 服務器缺省的行爲。同時,也有可能去實現一個異步的接口 RouteGuide::AsyncService,它容許你進一步定製服務器線程的行爲,雖然在本教程中咱們並不關注這點。

RouteGuideImpl 實現了全部的服務方法。讓咱們先來看看最簡單的類型 GetFeature,它從客戶端拿到一個 Point 而後將對應的特性返回給數據庫中的 Feature

Status GetFeature(ServerContext* context, const Point* point,
                    Feature* feature) override {
    feature->set_name(GetFeatureName(*point, feature_list_));
    feature->mutable_location()——>CopyFrom(*point);
    return Status::OK;
  }

這個方法爲 RPC 傳遞了一個上下文對象,包含了客戶端的 Point protocol buffer 請求以及一個填充響應信息的Feature protocol buffer。在這個方法中,咱們用適當的信息填充 Feature,而後返回OK的狀態,告訴 gRPC 咱們已經處理完 RPC,而且 Feature 能夠返回給客戶端。

如今讓咱們看看更加複雜點的狀況——流式RPC。 ListFeatures 是一個服務器端的流式 RPC,所以咱們須要給客戶端返回多個 Feature

Status ListFeatures(ServerContext* context, const Rectangle* rectangle,
                      ServerWriter<Feature>* writer) override {
    auto lo = rectangle->lo();
    auto hi = rectangle->hi();
    long left = std::min(lo.longitude(), hi.longitude());
    long right = std::max(lo.longitude(), hi.longitude());
    long top = std::max(lo.latitude(), hi.latitude());
    long bottom = std::min(lo.latitude(), hi.latitude());
    for (const Feature& f : feature_list_) {
      if (f.location().longitude() >= left &&
          f.location().longitude() <= right &&
          f.location().latitude() >= bottom &&
          f.location().latitude() <= top) {
        writer->Write(f);
      }
    }
    return Status::OK;
  }

如你所見,此次咱們拿到了一個請求對象(客戶端指望在 Rectangle 中找到的 Feature)以及一個特殊的 ServerWriter 對象,而不是在咱們的方法參數中獲取簡單的請求和響應對象。在方法中,根據返回的須要填充足夠多的 Feature 對象,用 ServerWriterWrite() 方法寫入。最後,和咱們簡單的 RPC 例子相同,咱們返回Status::OK去告知gRPC咱們已經完成了響應的寫入。

若是你看過客戶端流方法RecordRoute,你會發現它很相似,除了此次咱們拿到的是一個ServerReader而不是請求對象和單一的響應。咱們使用 ServerReaderRead() 方法去重複的往請求對象(在這個場景下是一個 Point)讀取客戶端的請求直到沒有更多的消息:在每次調用後,服務器須要檢查 Read() 的返回值。若是返回值爲 true,流仍然存在,它就能夠繼續讀取;若是返回值爲 false,則代表消息流已經中止。

while (stream->Read(&point)) {
  ...//process client input
}

最後,讓咱們看看雙向流RPCRouteChat()

Status RouteChat(ServerContext* context,
                   ServerReaderWriter<RouteNote, RouteNote>* stream) override {
    std::vector<RouteNote> received_notes;
    RouteNote note;
    while (stream->Read(&note)) {
      for (const RouteNote& n : received_notes) {
        if (n.location().latitude() == note.location().latitude() &&
            n.location().longitude() == note.location().longitude()) {
          stream->Write(n);
        }
      }
      received_notes.push_back(note);
    }

    return Status::OK;
  }

此次咱們獲得的 ServerReaderWriter 對象能夠用來讀 寫消息。這裏讀寫的語法和咱們客戶端流以及服務器流方法是同樣的。雖然每一端獲取對方信息的順序和寫入的順序一致,客戶端和服務器均可以以任意順序讀寫——流的操做是徹底獨立的。

啓動服務器

一旦咱們實現了全部的方法,咱們還須要啓動一個gRPC服務器,這樣客戶端纔可使用服務。下面這段代碼展現了在咱們RouteGuide服務中實現的過程:

void RunServer(const std::string& db_path) {
  std::string server_address("0.0.0.0:50051");
  RouteGuideImpl service(db_path);

  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();
}

如你所見,咱們經過使用ServerBuilder去構建和啓動服務器。爲了作到這點,咱們須要:

  1. 建立咱們的服務實現類 RouteGuideImpl 的一個實例。
  2. 建立工廠類 ServerBuilder 的一個實例。
  3. 在生成器的 AddListeningPort() 方法中指定客戶端請求時監聽的地址和端口。
  4. 用生成器註冊咱們的服務實現。
  5. 調用生成器的 BuildAndStart() 方法爲咱們的服務建立和啓動一個RPC服務器。
  6. 調用服務器的 Wait() 方法實現阻塞等待,直到進程被殺死或者 Shutdown() 被調用。

建立客戶端

在這部分,咱們將嘗試爲RouteGuide服務建立一個C++的客戶端。你能夠從examples/cpp/route_guide/route_guide_client.cc看到咱們完整的客戶端例子代碼.

建立一個存根

爲了能調用服務的方法,咱們得先建立一個 存根

首先須要爲咱們的存根建立一個gRPC channel,指定咱們想鏈接的服務器地址和端口,以及 channel 相關的參數——在本例中咱們使用了缺省的 ChannelArguments 而且沒有使用SSL:

grpc::CreateChannel("localhost:50051", grpc::InsecureCredentials(), ChannelArguments());

如今咱們能夠利用channel,使用從.proto中生成的RouteGuide類提供的NewStub方法去建立存根。

public:
  RouteGuideClient(std::shared_ptr<ChannelInterface> channel,
                   const std::string& db)
      : stub_(RouteGuide::NewStub(channel)) {
    ...
  }

調用服務的方法

如今咱們來看看如何調用服務的方法。注意,在本教程中調用的方法,都是 阻塞/同步 的版本:這意味着 RPC 調用會等待服務器響應,要麼返回響應,要麼引發一個異常。

簡單RPC

調用簡單 RPC GetFeature 幾乎是和調用一個本地方法同樣直觀。

Point point;
  Feature feature;
  point = MakePoint(409146138, -746188906);
  GetOneFeature(point, &feature);

...

  bool GetOneFeature(const Point& point, Feature* feature) {
    ClientContext context;
    Status status = stub_->GetFeature(&context, point, feature);
    ...
  }

如你所見,咱們建立而且填充了一個請求的 protocol buffer 對象(例子中爲 Point),同時爲了服務器填寫建立了一個響應 protocol buffer 對象。爲了調用咱們還建立了一個 ClientContext 對象——你能夠隨意的設置該對象上的配置的值,好比期限,雖然如今咱們會使用缺省的設置。注意,你不能在不一樣的調用間重複使用這個對象。最後,咱們在存根上調用這個方法,將其傳給上下文,請求以及響應。若是方法的返回是OK,那麼咱們就能夠從服務器從咱們的響應對象中讀取響應信息。

std::cout << "Found feature called " << feature->name()  << " at "
                << feature->location().latitude()/kCoordFactor_ << ", "
                << feature->location().longitude()/kCoordFactor_ << std::endl;

流式RPC

如今來看看咱們的流方法。若是你已經讀過建立服務器,本節的一些內容看上去很熟悉——流式 RPC 是在客戶端和服務器兩端以一種相似的方式實現的。下面就是咱們稱做是服務器端的流方法 ListFeatures,它會返回地理的 Feature

std::unique_ptr<ClientReader<Feature> > reader(
        stub_->ListFeatures(&context, rect));
    while (reader->Read(&feature)) {
      std::cout << "Found feature called "
                << feature.name() << " at "
                << feature.location().latitude()/kCoordFactor_ << ", "
                << feature.location().longitude()/kCoordFactor_ << std::endl;
    }
    Status status = reader->Finish();

咱們將上下文傳給方法而且請求,獲得 ClientReader 返回對象,而不是將上下文,請求和響應傳給方法。客戶端可使用 ClientReader 去讀取服務器的響應。咱們使用 ClientReaderRead() 反覆讀取服務器的響應到一個響應 protocol buffer 對象(在這個例子中是一個 Feature),直到沒有更多的消息:客戶端須要去檢查每次調用完 Read() 方法的返回值。若是返回值爲 true,流依然存在而且能夠持續讀取;若是是 false,說明消息流已經結束。最後,咱們在流上調用 Finish() 方法結束調用並獲取咱們 RPC 的狀態。

客戶端的流方法 RecordRoute 的使用很類似,除了咱們將一個上下文和響應對象傳給方法,拿到一個 ClientWriter 返回。

std::unique_ptr<ClientWriter<Point> > writer(
        stub_->RecordRoute(&context, &stats));
    for (int i = 0; i < kPoints; i++) {
      const Feature& f = feature_list_[feature_distribution(generator)];
      std::cout << "Visiting point "
                << f.location().latitude()/kCoordFactor_ << ", "
                << f.location().longitude()/kCoordFactor_ << std::endl;
      if (!writer->Write(f.location())) {
        // Broken stream.
        break;
      }
      std::this_thread::sleep_for(std::chrono::milliseconds(
          delay_distribution(generator)));
    }
    writer->WritesDone();
    Status status = writer->Finish();
    if (status.IsOk()) {
      std::cout << "Finished trip with " << stats.point_count() << " points\n"
                << "Passed " << stats.feature_count() << " features\n"
                << "Travelled " << stats.distance() << " meters\n"
                << "It took " << stats.elapsed_time() << " seconds"
                << std::endl;
    } else {
      std::cout << "RecordRoute rpc failed." << std::endl;
    }

一旦咱們用 Write() 將客戶端請求寫入到流的動做完成,咱們須要在流上調用 WritesDone() 通知 gRPC 咱們已經完成寫入,而後調用 Finish() 完成調用同時拿到 RPC 的狀態。若是狀態是 OK,咱們最初傳給 RecordRoute() 的響應對象會跟着服務器的響應被填充。

最後,讓咱們看看雙向流式 RPC RouteChat()。在這種場景下,咱們將上下文傳給一個方法,拿到一個能夠用來讀寫消息的ClientReaderWriter的返回。

std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote> > stream(
        stub_->RouteChat(&context));

這裏讀寫的語法和咱們客戶端流以及服務器端流方法沒有任何區別。雖然每一方都能按照寫入時的順序拿到另外一方的消息,客戶端和服務器端均可以以任意順序讀寫——流操做起來是徹底獨立的。

來試試吧!

構建客戶端和服務器:

$ make

運行服務器,它會監聽50051端口:

$ ./route_guide_server

在另一個終端運行客戶端:

$ ./route_guide_client
相關文章
相關標籤/搜索