Google gRPC

来源:互联网 发布:红颜知已歌曲人名 编辑:程序博客网 时间:2024/06/05 14:16

GRPC算是比较年轻的项目,虽据说已在Google内部被大规模部署使用,但从GitHub上看是2016年8月19日打的v1.0.0的tag,而官方博客发布声明在2016年8月23日。正式发布也就意味着通信协议的确定、接口API已经稳定,而性能、压力、稳定性各项测试已经满足需求,可以部署到生产环境中,广大基佬们可以安心使用了。

与gRPC/Protobuf相对应的,莫过于当前最熟悉的传统经典HTTP/JSON模式了,传统开发的惯用手法就是:客户端发起请求、服务端接收请求、服务端解析请求、服务端进行业务逻辑处理、服务端打包响应、服务端发送响应、客户端解析响应。虽然现在的序列化库和网络开发框架漫山遍野多如牛毛,但是服务端和客户端开发还是需要不断地封装、解析数据,处理网络传输的各项细节。

RPC从本质上来说,就是通过客户端和服务器的协作,将客户端的本地调用转化成请求发送到服务端,服务端进行实际操作后,再将结果返回给客户端,所以从客户端的角度看来就和一个本地调用的效果一样,虽然实际上跨进程、跨主机的调用会遇见各种复杂的情况,但是RPC框架负责屏蔽这些细节信息,用户只需要专注于业务逻辑开发即可。从Wikipedia的资料看来,RPC的概念很早就已经被提出来,而最近风光无限的几个开源RPC框架基本都出自大厂之手,其源于在互联网环境下,大量的分布式应用或服务可以使用RPC的方式轻松解耦,增加了复用性,提高了开发效率。

此外还想罗嗦一句:gRPC/Protobuf不仅可以用于常规网络服务开发,甚至可以作为本地进程间通信方式使用,因为RPC本来就属于一种IPC手段。


gRPC和Protobuf天生有着紧密的联系,在gRPC中Protobuf不仅作为一种序列化的工具使用,而且用于定义服务端与客户端之间的RPC调用接口(IDL的效果),然后通过protoc工具可以快速生成客户端和服务端的代码。gRPC允许通过Protobuf的插件,独立指定客户端和服务端生成的语言类型,这对于时下移动互联网时代的开发意义重大。Protobuf是一种重要的序列化工具,其编码效率和速率非常的高,而且在工程化的过程中Google考虑到前向兼容等各项事宜,简单的手册可以参见之前的 《Protobuf数据交换格式的使用方法》 。无论以后用哪家的RPC,都建议好好学习熟练掌握它,因为当前一些新开源的框架库基本都默认用它作为数据交互格式。

下面借着gRPC官方的手册,流水帐般地过一下gRPC的相关东西。

一、RPC生命周期

gRPC支持四种服务类型:Unary RPCs、Server streaming RPCs、Client streaming RPCs和Bidirectional streaming RPCs,通过参数和返回类型是否有stream关键字来标识区分。最简单的是Unary RPC调用,客户端发送一个请求参数,服务端做出一个应答数据;Server stream RPC调用是服务端可以返回多个数据,客户端一般在while中一直读取结束;Client stream是客户端可以向服务端传输多个请求,告知服务端传输结束后等待服务端返回;而Bidirectional stream则是一个全双工的通信,两端可以在任意时刻发送和接收数据,互相独立互不干扰。

gRPC允许client提供额外的超时参数,在超时之后如果服务端还没有返回响应的话,则会返回DEADLINE_EXCEEDED错误。服务端可以查询请求的超时参数,以及该调用所剩余的完成时间值。

RPC调用结果,是由服务端和客户端本地独立决定的,比如服务端认为自己成功发送了response,但是客户端可能在超时后仍然没有收到服务端响应,而认为此次调用失败,毕竟跨进程、跨主机的调用涉及到的可能问题会很多。

客户端和服务端可以在任何时候取消(cancel)RPC调用,取消的请求会立即生效,之后的工作不会再执行,同时之前的工作也不会undo进行回滚。客户端如果使用同步模式调用,一般是无法取消调用的,因为其执行流已经被block阻塞住了。

当创建Client Stub的时候,会要求和服务端的指定端口创建一个Channel通道,这个通道可以控制各项参数,以细致化地影响和控制gRPC的行为。

从上面描述,可见gRPC不保证原子性、最终一致性等特性,这个锅看来是甩给了用户处理了!

二、Authentication认证

gRPC/Protobuf原生支持SSL/TLS加密方式传输(Token模式暂不讨论),可以加密服务端和客户端的所有通信数据。

gRPC的认证都围绕着Credentials这个对象,分为ChannelCredentials和CallCredentials两种类型,也可以将两者关联成一个CompositeChannelCredentials,然后用其产生一个新的ChannelCredentials后,那么之后在这个Channel上所有的调用都会默认使用前面设置的CallCredentials。

(1). 无加密通信模式

auto channel = grpc::CreateChannel("localhost:50051", InsecureChannelCredentials());std::unique_ptr<Greeter::Stub> stub(Greeter::NewStub(channel));

(2). SSL/TSL通信

// Create a default SSL ChannelCredentials object.auto creds = grpc::SslCredentials(grpc::SslCredentialsOptions());// Create a channel using the credentials created in the previous step.auto channel = grpc::CreateChannel(server_name, creds);std::unique_ptr<Greeter::Stub> stub(Greeter::NewStub(channel));grpc::Status s = stub->sayHello(&context, *request, response);

三、gRPC/Protobuf C++语言使用实例

(1). 创建IDL描述文件route_guide.proto

gRPC是需要先定义服务接口约定,才可以进行RPC调用,使用.proto可以同时定义客户端和服务端交换的数据格式以及RPC调用的接口,然后使用protoc工具加上特定语言的插件生成特定语言版本的辅助代码。其实相比之前的 《Protobuf数据交换格式的使用方法》 ,这里只是新增了service定义和rpc定义的语法。

syntax = "proto3";package routeguide;service RouteGuide {  rpcGetFeature(Point)returns(Feature){}   // server-to-client streaming RPC.  rpcListFeatures(Rectangle)returns(stream Feature){}  // client-to-server streaming RPC.  rpcRecordRoute(stream Point)returns(RouteSummary){}  // Bidirectional streaming RPC.  rpcRouteChat(stream RouteNote)returns(stream RouteNote){}}// Latitudes +/- 90 degrees and longitude +/- 180 degrees (inclusive).message Point {  int32 latitude = 1;  int32 longitude = 2;}// A latitude-longitude rectanglemessage Rectangle {  Point lo = 1;  Point hi = 2;}// A feature names something at a given point.// If a feature could not be named, the name is empty.message Feature {  string name = 1; // The name of the feature.  Point location = 2;}// A RouteNote is a message sent while at a given point.message RouteNote {  Point location = 1;  string message = 2;}// A RouteSummary is received in response to a RecordRoute rpc.message RouteSummary {  int32 point_count = 1; // number of points received.  int32 feature_count = 2; // number of known features passed while traversing  int32 distance = 3; // distance covered in metres.  int32 elapsed_time = 4; // duration of the traversal}

(2). 使用protoc产生服务端和客户端代码

通过protoc和C++ plugin,可以产生C++的服务端和客户端代码

$ protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../protos/route_guide.proto$ protoc --cpp_out=. ../protos/route_guide.proto

上面的操作会分别产生数据交换和服务接口的源文件:route_guide.pb.[cc|h]和route_guide.grpc.pb.[cc|h],前者覆盖所有数据类型序访问和操作的接口,后者主要生成定义的RPC服务RouteGuide的相关代码。

grpc_out参数编译之后的源代码,主要产生了class RouteGuide,包含了和客户端Stub相关的内部类class StubInterface和class Stub GRPC_FINAL : public StubInterface,以及和服务端相关的class Service : public ::grpc::Service类。

(3). 实现服务端业务接口

通过上面步骤,操作数据和服务接口相关代码都已经自动生成了,接下来服务端的重点就是接口业务逻辑的实现了。手册样例的服务端代码实现在route_guide_server.cc源文件中,通过实现RouteGuide::Service中定义的虚函数接口,可以实现以同步阻塞方式的服务端实现(class RouteGuideImpl final : public RouteGuide::Service),而异步方式则跟RouteGuide::AsyncService这个类相关。

此处服务端首先需要实现proto service中定义的四个调用接口,通过观察这些虚函数接口,发现他们都是返回::grpc::Status类型(返回值的详细信息可以参看 Error model ),并且第一个参数都是::grpc::ServerContext*,而剩余部分的参数就跟当初proto文件中声明的参数一致了。在函数的具体实现代码中,设置和获取proto的数据项,就是Protobuf的标准数据操作方式了。通常操作成功后,返回Status::OK。

Status GetFeature(ServerContext* context,constPoint* point,        Feature* feature) override {  feature->set_name(GetFeatureName(*point, feature_list_));  feature->mutable_location()->CopyFrom(*point);  return Status::OK;}

上面代码是最简单的Unary调用方式,客户端发出一个请求参数,然后服务端返回一个数据响应。对于RPC的服务端,在使用stream模式的调用参数或者返回结果,需要使用到特殊的ServerWriter、ServerReader类型,服务端可以在循环中多次写入/读取以传递多个对象,最后返回Status状态以表示调用结束。

for (const Feature& f : feature_list_) {  ... writer->Write(f);}while (reader->Read(&point)) { ... }

对于请求和响应都是stream的类型,那么参数将直接变成为ServerReaderWriter

* stream类型,此时的stream是一个两方向完全独立的全双工信道。

while (stream->Read(¬e)) {    for (const RouteNote& n : received_notes) {      if ( ... )        stream->Write(n);    }    received_notes.push_back(note);}

当把RouteGuide::Service中的虚函数接口全部实现后,服务端的业务开发也就完成了。下面是通用的服务端网络例程,绑定地址端口,接收客户端请求,十分的清晰明白:

voidRunServer(conststd::string& db_path){  std::stringserver_address("0.0.0.0:50051");  RouteGuideImplservice(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(); // until killed or server->Shutdown()}intmain(intargc,char** argv){  std::string db = routeguide::GetDbFileContent(argc, argv);  RunServer(db);  return 0;}

(4). 创建客户端

客户端的业务代码定义在route_guide_client.cc源文件中。在RPC的调用体系中,业务相关的代码都已经实现在服务端,所以通常来说客户端会定义同服务端接口相同的函数名(非必需),然后在这些函数实现中,完成对服务端的RPC调用,并获取调用返回的结果。

客户端在初始化的时候,需要首先创建grpc::Channel和RouteGuide::Stub两个对象。

std::shared_ptr<Channel> channel = grpc::CreateChannel("localhost:50051",                          grpc::InsecureChannelCredentials();std::unique_ptr<RouteGuide::Stub> stub_ = RouteGuide::NewStub(channel);

上面是用的简单非加密方式创建的Channel。然后,客户端通过stub_对象就可以直接进行RPC调用了

Point point = MakePoint(409146138, -746188906);Feature feature;GetOneFeature(point, &feature);boolGetOneFeature(constPoint& point, Feature* feature){  ClientContext context;  Status status = stub_->GetFeature(&context, point, feature);  ...}

可见,每次调用都需要传入一个context对象的地址,上面是进行的默认构造,可以通过对这个context对象设置RPC调用相关的细节参数(比如超时等)。因为在不同的RPC调用之间不能共享这个对象,所以其一般都是以局部自动对象的方式创建的。

上面的Unary调用是最简单的情况。对于stream类型的调用,客户端同样有与服务端相似的ClientReader、ClientWriter以及ClientReaderWriter对象来完成相关操作。这些对象可以调用Finish()来获取服务端返回来的RPC调用状态,而WritesDone()可以显式通知对端写入完成,特别适合client-to-server streaming RPC类型的调用。

while (reader->Read(&feature)) { ... }Status status = reader->Finish();std::unique_ptr<ClientWriter<Point> > writer(    stub_->RecordRoute(&context, &stats));for (int i = 0; i < kPoints; i++) {  if (!writer->Write(f.location())) {    break;     // Broken stream.  }  std::this_thread::sleep_for(std::chrono::milliseconds(      delay_distribution(generator)));}writer->WritesDone();Status status = writer->Finish();if (status.IsOk()) { ... }



1 0