文章來(lái)自gRPC 官方文檔中文版
本教程提供了C++程序員如何使用gRPC的指南。
通過(guò)學(xué)習(xí)教程中例子,你可以學(xué)會(huì)如何:
- 在一個(gè) .proto 文件內(nèi)定義服務(wù).
- 用 protocol buffer 編譯器生成服務(wù)器和客戶端代碼.
- 使用 gRPC 的 C++ API 為你的服務(wù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的客戶端和服務(wù)器.
假設(shè)你已經(jīng)閱讀了概覽并且熟悉protocol buffers. 注意,教程中的例子使用的是 protocol buffers 語(yǔ)言的 proto3 版本,它目前只是 alpha 版:可以在proto3 語(yǔ)言指南和 protocol buffers 的 Github 倉(cāng)庫(kù)的版本注釋發(fā)現(xiàn)更多關(guān)于新版本的內(nèi)容.
這算不上是一個(gè)在 C++ 中使用 gRPC 的綜合指南:以后會(huì)有更多的參考文檔.
為什么使用 gRPC?
我們的例子是一個(gè)簡(jiǎn)單的路由映射的應(yīng)用,它允許客戶端獲取路由特性的信息,生成路由的總結(jié),以及交互路由信息,如服務(wù)器和其他客戶端的流量更新。
有了 gRPC, 我們可以一次性的在一個(gè) .proto 文件中定義服務(wù)并使用任何支持它的語(yǔ)言去實(shí)現(xiàn)客戶端和服務(wù)器,反過(guò)來(lái),它們可以在各種環(huán)境中,從Google的服務(wù)器到你自己的平板電腦- gRPC 幫你解決了不同語(yǔ)言間通信的復(fù)雜性以及環(huán)境的不同.使用 protocol buffers 還能獲得其他好處,包括高效的序列號(hào),簡(jiǎn)單的 IDL 以及容易進(jìn)行接口更新。
例子代碼和設(shè)置
教程的代碼在這里 grpc/grpc/examples/cpp/route_guide. 要下載例子,通過(guò)運(yùn)行下面的命令去克隆grpc 代碼庫(kù):
$ git clone https://github.com/grpc/grpc.git
改變當(dāng)前的目錄到examples/cpp/route_guide :
$ cd examples/cpp/route_guide
你還需要安裝生成服務(wù)器和客戶端的接口代碼相關(guān)工具-如果你還沒(méi)有安裝的話,查看下面的設(shè)置指南 C++快速開(kāi)始指南。
定義服務(wù)
我們的第一步(可以從概覽中得知)是使用 protocol buffers去定義 gRPC service 和方法 request 以及 response 的類型。你可以在examples/protos/route_guide.proto 看到完整的 .proto 文件。
要定義一個(gè)服務(wù),你必須在你的 .proto 文件中指定 service :
service RouteGuide {
...
}
然后在你的服務(wù)中定義 rpc 方法,指定請(qǐng)求的和響應(yīng)類型。gRPC允 許你定義4種類型的 service 方法,在 RouteGuide 服務(wù)中都有使用:
- 一個(gè) 簡(jiǎn)單 RPC , 客戶端使用存根發(fā)送請(qǐng)求到服務(wù)器并等待響應(yīng)返回,就像平常的函數(shù)調(diào)用一樣。
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- 一個(gè) 服務(wù)器端流式 RPC , 客戶端發(fā)送請(qǐng)求到服務(wù)器,拿到一個(gè)流去讀取返回的消息序列。 客戶端讀取返回的流,直到里面沒(méi)有任何消息。從例子中可以看出,通過(guò)在 響應(yīng) 類型前插入
stream 關(guān)鍵字,可以指定一個(gè)服務(wù)器端的流方法。
// 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) {}
- 一個(gè) 客戶端流式 RPC , 客戶端寫入一個(gè)消息序列并將其發(fā)送到服務(wù)器,同樣也是使用流。一旦客戶端完成寫入消息,它等待服務(wù)器完成讀取返回它的響應(yīng)。通過(guò)在 請(qǐng)求 類型前指定
stream 關(guān)鍵字來(lái)指定一個(gè)客戶端的流方法。
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 一個(gè) 雙向流式 RPC 是雙方使用讀寫流去發(fā)送一個(gè)消息序列。兩個(gè)流獨(dú)立操作,因此客戶端和服務(wù)器可以以任意喜歡的順序讀寫:比如, 服務(wù)器可以在寫入響應(yīng)前等待接收所有的客戶端消息,或者可以交替的讀取和寫入消息,或者其他讀寫的組合。 每個(gè)流中的消息順序被預(yù)留。你可以通過(guò)在請(qǐng)求和響應(yīng)前加
stream 關(guān)鍵字去制定方法的類型。
// 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 文件也包含了所有請(qǐng)求的 protocol buffer 消息類型定義以及在服務(wù)方法中使用的響應(yīng)類型-比如,下面的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;
}
生成客戶端和服務(wù)器端代碼
接下來(lái)我們需要從 .proto 的服務(wù)定義中生成 gRPC 客戶端和服務(wù)器端的接口。我們通過(guò) protocol buffer 的編譯器 protoc 以及一個(gè)特殊的 gRPC C++ 插件來(lái)完成。
簡(jiǎn)單起見(jiàn),我們提供一個(gè) makefile 幫您用合適的插件,輸入,輸出去運(yùn)行 protoc (如果你想自己去運(yùn)行,確保你已經(jīng)安裝了 protoc,并且請(qǐng)遵循下面的 gRPC 代碼安裝指南)來(lái)操作:
$ make route_guide.grpc.pb.cc route_guide.pb.cc
實(shí)際上運(yùn)行的是:
$ 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
運(yùn)行這個(gè)命令可以在當(dāng)前目錄中生成下面的文件:
route_guide.pb.h , 聲明生成的消息類的頭文件route_guide.pb.cc , 包含消息類的實(shí)現(xiàn)route_guide.grpc.pb.h , 聲明你生成的服務(wù)類的頭文件route_guide.grpc.pb.cc , 包含服務(wù)類的實(shí)現(xiàn)
這些包括:
- 所有的填充,序列化和獲取我們請(qǐng)求和響應(yīng)消息類型的 protocol buffer 代碼
- 名為
RouteGuide 的類,包含
- 為了客戶端去調(diào)用定義在
RouteGuide 服務(wù)的遠(yuǎn)程接口類型(或者 存根 ) - 讓服務(wù)器去實(shí)現(xiàn)的兩個(gè)抽象接口,同時(shí)包括定義在
RouteGuide 中的方法。
創(chuàng)建服務(wù)器
首先來(lái)看看我們?nèi)绾蝿?chuàng)建一個(gè) RouteGuide 服務(wù)器。如果你只對(duì)創(chuàng)建 gRPC 客戶端感興趣,你可以跳過(guò)這個(gè)部分,直接到創(chuàng)建客戶端 (當(dāng)然你也可能發(fā)現(xiàn)它也很有意思)。
讓 RouteGuide 服務(wù)工作有兩個(gè)部分:
- 實(shí)現(xiàn)我們服務(wù)定義的生成的服務(wù)接口:做我們的服務(wù)的實(shí)際的“工作”。
- 運(yùn)行一個(gè) gRPC 服務(wù)器,監(jiān)聽(tīng)來(lái)自客戶端的請(qǐng)求并返回服務(wù)的響應(yīng)。
你可以從examples/cpp/route_guide/route_guide_server.cc看到我們的 RouteGuide 服務(wù)器的實(shí)現(xiàn)代碼?,F(xiàn)在讓我們近距離研究它是如何工作的。
實(shí)現(xiàn)RouteGuide
我們可以看出,服務(wù)器有一個(gè)實(shí)現(xiàn)了生成的 RouteGuide::Service 接口的 RouteGuideImpl 類:
class RouteGuideImpl final : public RouteGuide::Service {
...
}
在這個(gè)場(chǎng)景下,我們正在實(shí)現(xiàn) 同步 版本的RouteGuide ,它提供了 gRPC 服務(wù)器缺省的行為。同時(shí),也有可能去實(shí)現(xiàn)一個(gè)異步的接口 RouteGuide::AsyncService ,它允許你進(jìn)一步定制服務(wù)器線程的行為,雖然在本教程中我們并不關(guān)注這點(diǎn)。
RouteGuideImpl 實(shí)現(xiàn)了所有的服務(wù)方法。讓我們先來(lái)看看最簡(jiǎn)單的類型 GetFeature ,它從客戶端拿到一個(gè) Point 然后將對(duì)應(yīng)的特性返回給數(shù)據(jù)庫(kù)中的 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;
}
這個(gè)方法為 RPC 傳遞了一個(gè)上下文對(duì)象,包含了客戶端的 Point protocol buffer 請(qǐng)求以及一個(gè)填充響應(yīng)信息的Feature protocol buffer。在這個(gè)方法中,我們用適當(dāng)?shù)男畔⑻畛?Feature ,然后返回OK 的狀態(tài),告訴 gRPC 我們已經(jīng)處理完 RPC,并且 Feature 可以返回給客戶端。
現(xiàn)在讓我們看看更加復(fù)雜點(diǎn)的情況——流式RPC。 ListFeatures 是一個(gè)服務(wù)器端的流式 RPC,因此我們需要給客戶端返回多個(gè) 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;
}
如你所見(jiàn),這次我們拿到了一個(gè)請(qǐng)求對(duì)象(客戶端期望在 Rectangle 中找到的 Feature )以及一個(gè)特殊的 ServerWriter 對(duì)象,而不是在我們的方法參數(shù)中獲取簡(jiǎn)單的請(qǐng)求和響應(yīng)對(duì)象。在方法中,根據(jù)返回的需要填充足夠多的 Feature 對(duì)象,用 ServerWriter 的 Write() 方法寫入。最后,和我們簡(jiǎn)單的 RPC 例子相同,我們返回Status::OK 去告知gRPC我們已經(jīng)完成了響應(yīng)的寫入。
如果你看過(guò)客戶端流方法RecordRoute ,你會(huì)發(fā)現(xiàn)它很類似,除了這次我們拿到的是一個(gè)ServerReader 而不是請(qǐng)求對(duì)象和單一的響應(yīng)。我們使用 ServerReader 的 Read() 方法去重復(fù)的往請(qǐng)求對(duì)象(在這個(gè)場(chǎng)景下是一個(gè) Point )讀取客戶端的請(qǐng)求直到?jīng)]有更多的消息:在每次調(diào)用后,服務(wù)器需要檢查 Read() 的返回值。如果返回值為 true ,流仍然存在,它就可以繼續(xù)讀取;如果返回值為 false ,則表明消息流已經(jīng)停止。
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 對(duì)象可以用來(lái)讀 和 寫消息。這里讀寫的語(yǔ)法和我們客戶端流以及服務(wù)器流方法是一樣的。雖然每一端獲取對(duì)方信息的順序和寫入的順序一致,客戶端和服務(wù)器都可以以任意順序讀寫——流的操作是完全獨(dú)立的。
啟動(dòng)服務(wù)器
一旦我們實(shí)現(xiàn)了所有的方法,我們還需要啟動(dòng)一個(gè)gRPC服務(wù)器,這樣客戶端才可以使用服務(wù)。下面這段代碼展示了在我們RouteGuide 服務(wù)中實(shí)現(xiàn)的過(guò)程:
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();
}
如你所見(jiàn),我們通過(guò)使用ServerBuilder 去構(gòu)建和啟動(dòng)服務(wù)器。為了做到這點(diǎn),我們需要:
- 創(chuàng)建我們的服務(wù)實(shí)現(xiàn)類
RouteGuideImpl 的一個(gè)實(shí)例。 - 創(chuàng)建工廠類
ServerBuilder 的一個(gè)實(shí)例。 - 在生成器的
AddListeningPort() 方法中指定客戶端請(qǐng)求時(shí)監(jiān)聽(tīng)的地址和端口。 - 用生成器注冊(cè)我們的服務(wù)實(shí)現(xiàn)。
- 調(diào)用生成器的
BuildAndStart() 方法為我們的服務(wù)創(chuàng)建和啟動(dòng)一個(gè)RPC服務(wù)器。 - 調(diào)用服務(wù)器的
Wait() 方法實(shí)現(xiàn)阻塞等待,直到進(jìn)程被殺死或者 Shutdown() 被調(diào)用。
創(chuàng)建客戶端
在這部分,我們將嘗試為RouteGuide 服務(wù)創(chuàng)建一個(gè)C++的客戶端。你可以從examples/cpp/route_guide/route_guide_client.cc看到我們完整的客戶端例子代碼.
創(chuàng)建一個(gè)存根
為了能調(diào)用服務(wù)的方法,我們得先創(chuàng)建一個(gè) 存根。
首先需要為我們的存根創(chuàng)建一個(gè)gRPC channel,指定我們想連接的服務(wù)器地址和端口,以及 channel 相關(guān)的參數(shù)——在本例中我們使用了缺省的 ChannelArguments 并且沒(méi)有使用SSL:
grpc::CreateChannel("localhost:50051", grpc::InsecureCredentials(), ChannelArguments());
現(xiàn)在我們可以利用channel,使用從.proto中生成的RouteGuide 類提供的NewStub 方法去創(chuàng)建存根。
public:
RouteGuideClient(std::shared_ptr<ChannelInterface> channel,
const std::string& db)
: stub_(RouteGuide::NewStub(channel)) {
...
}
調(diào)用服務(wù)的方法
現(xiàn)在我們來(lái)看看如何調(diào)用服務(wù)的方法。注意,在本教程中調(diào)用的方法,都是 阻塞/同步 的版本:這意味著 RPC 調(diào)用會(huì)等待服務(wù)器響應(yīng),要么返回響應(yīng),要么引起一個(gè)異常。
簡(jiǎn)單RPC
調(diào)用簡(jiǎn)單 RPC GetFeature 幾乎是和調(diào)用一個(gè)本地方法一樣直觀。
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);
...
}
如你所見(jiàn),我們創(chuàng)建并且填充了一個(gè)請(qǐng)求的 protocol buffer 對(duì)象(例子中為 Point ),同時(shí)為了服務(wù)器填寫創(chuàng)建了一個(gè)響應(yīng) protocol buffer 對(duì)象。為了調(diào)用我們還創(chuàng)建了一個(gè) ClientContext 對(duì)象——你可以隨意的設(shè)置該對(duì)象上的配置的值,比如期限,雖然現(xiàn)在我們會(huì)使用缺省的設(shè)置。注意,你不能在不同的調(diào)用間重復(fù)使用這個(gè)對(duì)象。最后,我們?cè)诖娓险{(diào)用這個(gè)方法,將其傳給上下文,請(qǐng)求以及響應(yīng)。如果方法的返回是OK ,那么我們就可以從服務(wù)器從我們的響應(yīng)對(duì)象中讀取響應(yīng)信息。
std::cout << "Found feature called " << feature->name() << " at "
<< feature->location().latitude()/kCoordFactor_ << ", "
<< feature->location().longitude()/kCoordFactor_ << std::endl;
流式RPC
現(xiàn)在來(lái)看看我們的流方法。如果你已經(jīng)讀過(guò)創(chuàng)建服務(wù)器,本節(jié)的一些內(nèi)容看上去很熟悉——流式 RPC 是在客戶端和服務(wù)器兩端以一種類似的方式實(shí)現(xiàn)的。下面就是我們稱作是服務(wù)器端的流方法 ListFeatures ,它會(huì)返回地理的 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();
我們將上下文傳給方法并且請(qǐng)求,得到 ClientReader 返回對(duì)象,而不是將上下文,請(qǐng)求和響應(yīng)傳給方法??蛻舳丝梢允褂?ClientReader 去讀取服務(wù)器的響應(yīng)。我們使用 ClientReader 的 Read() 反復(fù)讀取服務(wù)器的響應(yīng)到一個(gè)響應(yīng) protocol buffer 對(duì)象(在這個(gè)例子中是一個(gè) Feature ),直到?jīng)]有更多的消息:客戶端需要去檢查每次調(diào)用完 Read() 方法的返回值。如果返回值為 true ,流依然存在并且可以持續(xù)讀取;如果是 false ,說(shuō)明消息流已經(jīng)結(jié)束。最后,我們?cè)诹魃险{(diào)用 Finish() 方法結(jié)束調(diào)用并獲取我們 RPC 的狀態(tài)。
客戶端的流方法 RecordRoute 的使用很相似,除了我們將一個(gè)上下文和響應(yīng)對(duì)象傳給方法,拿到一個(gè) 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() 將客戶端請(qǐng)求寫入到流的動(dòng)作完成,我們需要在流上調(diào)用 WritesDone() 通知 gRPC 我們已經(jīng)完成寫入,然后調(diào)用 Finish() 完成調(diào)用同時(shí)拿到 RPC 的狀態(tài)。如果狀態(tài)是 OK ,我們最初傳給 RecordRoute() 的響應(yīng)對(duì)象會(huì)跟著服務(wù)器的響應(yīng)被填充。
最后,讓我們看看雙向流式 RPC RouteChat() 。在這種場(chǎng)景下,我們將上下文傳給一個(gè)方法,拿到一個(gè)可以用來(lái)讀寫消息的ClientReaderWriter 的返回。
std::shared_ptr<ClientReaderWriter<RouteNote, RouteNote> > stream(
stub_->RouteChat(&context));
這里讀寫的語(yǔ)法和我們客戶端流以及服務(wù)器端流方法沒(méi)有任何區(qū)別。雖然每一方都能按照寫入時(shí)的順序拿到另一方的消息,客戶端和服務(wù)器端都可以以任意順序讀寫——流操作起來(lái)是完全獨(dú)立的。
來(lái)試試吧!
構(gòu)建客戶端和服務(wù)器:
$ make
運(yùn)行服務(wù)器,它會(huì)監(jiān)聽(tīng)50051端口:
$ ./route_guide_server
在另外一個(gè)終端運(yùn)行客戶端:
$ ./route_guide_client
|