gRPC官⽅⽂档(gRPC基础:C++)
⽂章来⾃
本教程提供了C++程序员如何使⽤gRPC的指南。
通过学习教程中例⼦,你可以学会如何:
在⼀个 .proto ⽂件内定义服务.
⽤ protocol buffer 编译器⽣成服务器和客户端代码.
使⽤ gRPC 的 C++ API 为你的服务实现⼀个简单的客户端和服务器.
假设你已经阅读了并且熟悉. 注意,教程中的例⼦使⽤的是 protocol buffers 语⾔的 proto3 版本,它⽬前只是 alpha 版:可以在和 protocol buffers 的 Github 仓库的发现更多关于新版本的内容.
这算不上是⼀个在 C++ 中使⽤ gRPC 的综合指南:以后会有更多的参考⽂档.
为什么使⽤ gRPC?
我们的例⼦是⼀个简单的路由映射的应⽤,它允许客户端获取路由特性的信息,⽣成路由的总结,以及交互路由信息,如服务器和其他客户端的流量更新。
有了 gRPC,我们可以⼀次性的在⼀个 .proto ⽂件中定义服务并使⽤任何⽀持它的语⾔去实现客户端和服务器,反过来,它们可以在各种环境中,从Google的服务器到你⾃⼰的平板电脑- gRPC 帮你解决了不同语⾔间通信的复杂性以及环境的不同.使⽤ protocol buffers 还能获得其他好处,包括⾼效的序列号,简单的 IDL 以及容易进⾏接⼝更新。
例⼦代码和设置
教程的代码在这⾥ . 要下载例⼦,通过运⾏下⾯的命令去克隆grpc代码库:
$ git clone github/grpc/grpc.git
改变当前的⽬录到examples/cpp/route_guide:
$ cd examples/cpp/route_guide
你还需要安装⽣成服务器和客户端的接⼝代码相关⼯具-如果你还没有安装的话,查看下⾯的设置指南。
定义服务
我们的第⼀步(可以从中得知)是使⽤去定义 gRPC service和⽅法request以及response的类型。你可以在看到完整的 .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++ 插件来完成。
简单起见,我们提供⼀个帮您⽤合适的插件,输⼊,输出去运⾏protoc(如果你想⾃⼰去运⾏,确保你已经安装了 protoc,并且请遵循下⾯的gRPC 代码)来操作:
$ make route_
实际上运⾏的是:
$ 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_,包含消息类的实现
pc.pb.h,声明你⽣成的服务类的头⽂件
,包含服务类的实现
这些包括:
所有的填充,序列化和获取我们请求和响应消息类型的 protocol buffer 代码
名为RouteGuide的类,包含
为了客户端去调⽤定义在RouteGuide服务的远程接⼝类型(或者存根 )
让服务器去实现的两个抽象接⼝,同时包括定义在RouteGuide中的⽅法。
创建服务器
⾸先来看看我们如何创建⼀个RouteGuide服务器。如果你只对创建 gRPC 客户端感兴趣,你可以跳过这个部分,直接到 (当然你也可能发现它也很有意思)。
让RouteGuide服务⼯作有两个部分:
实现我们服务定义的⽣成的服务接⼝:做我们的服务的实际的“⼯作”。
运⾏⼀个 gRPC 服务器,监听来⾃客户端的请求并返回服务的响应。
你可以从看到我们的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)) {route add命令实例
...//process client input
}
最后,让我们看看双向流RPC RouteChat()。
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去构建和启动服务器。为了做到这点,我们需要:
1. 创建我们的服务实现类RouteGuideImpl的⼀个实例。
2. 创建⼯⼚类ServerBuilder的⼀个实例。
3. 在⽣成器的AddListeningPort()⽅法中指定客户端请求时监听的地址和端⼝。
4. ⽤⽣成器注册我们的服务实现。
5. 调⽤⽣成器的BuildAndStart()⽅法为我们的服务创建和启动⼀个RPC服务器。
6. 调⽤服务器的Wait()⽅法实现阻塞等待,直到进程被杀死或者Shutdown()被调⽤。
创建客户端
在这部分,我们将尝试为RouteGuide服务创建⼀个C++的客户端。你可以从看到我们完整的客户端例⼦代码.
创建⼀个存根
为了能调⽤服务的⽅法,我们得先创建⼀个存根。
⾸先需要为我们的存根创建⼀个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
在另外⼀个终端运⾏客户端:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论