文章來自gRPC 官方文檔中文版
本教程提供了C++程序員如何使用gRPC的指南。
通過學習教程中例子,你可以學會如何:
- 在一個 .proto 文件內定義服務.
- 用 protocol buffer 編譯器生成服務器和客戶端代碼.
- 使用 gRPC 的 C++ API 為你的服務實現一個簡單的客戶端和服務器.
假設你已經閱讀了概覽並且熟悉protocol buffers. 注意,教程中的例子使用的是 protocol buffers 語言的 proto3 版本,它目前只是 alpha 版:可以在proto3 語言指南和 protocol buffers 的 Github 倉庫的版本注釋發現更多關於新版本的內容.
這算不上是一個在 C++ 中使用 gRPC 的綜合指南:以后會有更多的參考文檔.
為什么使用 gRPC?
我們的例子是一個簡單的路由映射的應用,它允許客戶端獲取路由特性的信息,生成路由的總結,以及交互路由信息,如服務器和其他客戶端的流量更新。
有了 gRPC, 我們可以一次性的在一個 .proto 文件中定義服務並使用任何支持它的語言去實現客戶端和服務器,反過來,它們可以在各種環境中,從Google的服務器到你自己的平板電腦- gRPC 幫你解決了不同語言間通信的復雜性以及環境的不同.使用 protocol buffers 還能獲得其他好處,包括高效的序列號,簡單的 IDL 以及容易進行接口更新。
例子代碼和設置
教程的代碼在這里 grpc/grpc/examples/cpp/route_guide. 要下載例子,通過運行下面的命令去克隆grpc代碼庫:
$ 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 對象,用 ServerWriter 的 Write() 方法寫入。最后,和我們簡單的 RPC 例子相同,我們返回Status::OK去告知gRPC我們已經完成了響應的寫入。
如果你看過客戶端流方法RecordRoute,你會發現它很類似,除了這次我們拿到的是一個ServerReader而不是請求對象和單一的響應。我們使用 ServerReader 的 Read() 方法去重復的往請求對象(在這個場景下是一個 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(¬e)) {
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去構建和啟動服務器。為了做到這點,我們需要:
- 創建我們的服務實現類
RouteGuideImpl的一個實例。 - 創建工廠類
ServerBuilder的一個實例。 - 在生成器的
AddListeningPort()方法中指定客戶端請求時監聽的地址和端口。 - 用生成器注冊我們的服務實現。
- 調用生成器的
BuildAndStart()方法為我們的服務創建和啟動一個RPC服務器。 - 調用服務器的
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 去讀取服務器的響應。我們使用 ClientReader 的 Read() 反復讀取服務器的響應到一個響應 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
