文章來自gRPC 官方文檔中文版html
本教程提供了C++程序員如何使用gRPC的指南。git
經過學習教程中例子,你能夠學會如何:程序員
假設你已經閱讀了概覽而且熟悉protocol buffers. 注意,教程中的例子使用的是 protocol buffers 語言的 proto3 版本,它目前只是 alpha 版:能夠在proto3 語言指南和 protocol buffers 的 Github 倉庫的版本註釋發現更多關於新版本的內容.github
這算不上是一個在 C++ 中使用 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
服務中都有使用:
// Obtains the feature at a given position. rpc GetFeature(Point) returns (Feature) {}
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) {}
stream
關鍵字來指定一個客戶端的流方法。// Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {}
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
, 包含服務類的實現這些包括:
RouteGuide
的類,包含
RouteGuide
服務的遠程接口類型(或者 存根 )RouteGuide
中的方法。首先來看看咱們如何建立一個 RouteGuide
服務器。若是你只對建立 gRPC 客戶端感興趣,你能夠跳過這個部分,直接到建立客戶端 (固然你也可能發現它也頗有意思)。
讓 RouteGuide
服務工做有兩個部分:
你能夠從examples/cpp/route_guide/route_guide_server.cc看到咱們的 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 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 是在客戶端和服務器兩端以一種相似的方式實現的。下面就是咱們稱做是服務器端的流方法 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