参考
通过学习教程中例子,你可以学会如何:
为什么使用 gRPC?
我们的例子是一个简单的路由映射的应用,它允许客户端获取路由特性的信息,生成路由的总结,以及交互路由信息,如服务器和其他客户端的流量更新。
有了 gRPC, 我们可以一次性的在一个 .proto 文件中定义服务并使用任何支持它的语言去实现客户端和服务器,反过来,它们可以在各种环境中,从Google的服务器到你自己的平板电脑- gRPC 帮你解决了不同语言间通信的复杂性以及环境的不同.使用 protocol buffers 还能获得其他好处,包括高效的序列号,简单的 IDL 以及容易进行接口更新。
$ git clone https://github.com/grpc/grpc.git
改变当前的目录到examples/cpp/route_guide:
$ cd examples/cpp/route_guide
我们的第一步(可以从概览中得知)是使用 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) {}
// 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) {}
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// 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
运行这个命令可以在当前目录中生成下面的文件:
这些包括:
首先来看看我们如何创建一个 RouteGuide 服务器。如果你只对创建 gRPC 客户端感兴趣,你可以跳过这个部分,直接到创建客户端 (当然你也可能发现它也很有意思)。
让 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* 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* stream) override {
std::vector 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("124.0.0.1:50051");
RouteGuideImpl service(db_path);
ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait();
}
如你所见,我们通过使用ServerBuilder去构建和启动服务器。为了做到这点,我们需要:
在这部分,我们将尝试为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 channel,
const std::string& db)
: stub_(RouteGuide::NewStub(channel)) {
...
}
现在我们来看看如何调用服务的方法。注意,在本教程中调用的方法,都是 阻塞/同步 的版本:这意味着 RPC 调用会等待服务器响应,要么返回响应,要么引起一个异常。
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;
std::unique_ptr > 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 > 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 > stream(
stub_->RouteChat(&context));
这里读写的语法和我们客户端流以及服务器端流方法没有任何区别。虽然每一方都能按照写入时的顺序拿到另一方的消息,客户端和服务器端都可以以任意顺序读写——流操作起来是完全独立的。
make
# 运行服务器,它会监听50051端口:
./route_guide_server
# 在另外一个终端运行客户端:
./route_guide_client