文章来自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