复利效应
gRPC是Google开源的RPC框架,拥有高性能、跨语言等诸多优点。gRPC官方网站为grpc.io。鉴于官网的介绍较为混乱,并且其教程并不完善易懂,故而这里做一个简单的整理,希望一起成长。
本文所采用编程语言为C++,其他语言可以参考,开发平台为Windows平台。
gRPC基于Protocol Buffer,在使用gRPC时,一般都是按照下列步骤:
上图是gRPC原理图,gRPC服务端实现具体的RPC服务,客户端通过gRPC Stub来调用这些RPC服务。客户端和服务端是通过信道( Channel
)来连接的。
gRPC有四种使用场景:单向RPC(一问一答)、服务端流式RPC(一问多答)、客户端流式RPC(多问一答)、双向流式RPC(多问多答)。gRPC的调用方式又分为同步(阻塞)和异步(非阻塞),所以我们需要根据需求,来选择使用场景和调用方式。四类服务方法如下:
rpc sayHello(HelloRequest) returns(HelloResponse) { }
rpc LotsOfReplies(HelloRequest) returns(stream HelloResponse) { }
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
对于gRPC使用场景,在生成RPC代码时,都会生成同步和异步接口。下文中会给出一个简单的同步单向gRPC示例,再次基础上我们会分析gRPC的详细代码。
有关gRPC的示例代码,都可以从 Github: gRPC-Guide 获取。
这里需要有Protocol Buffer基础,具体使用可以Google。
# hello.proto syntax="proto3"; package guide; message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } service HelloSvc { rpc sayHello(HelloRequest) returns(HelloResponse); }
为了显示目录结构,下面的RPC代码借助了cmake。 protoc.exe
可以从protobuf网站下载,也可以自己编译。
EXECUTE_PROCESS(COMMAND ${CMAKE_SOURCE_DIR}/thirdparty/gRPC/third_party/protobuf/cmake/win/Debug/protoc.exe -I ${CMAKE_SOURCE_DIR}/protos/guide --grpc_out=${CMAKE_SOURCE_DIR}/sync/client/src --grpc_out=${CMAKE_SOURCE_DIR}/sync/server/src --cpp_out=${CMAKE_SOURCE_DIR}/sync/client/src --cpp_out=${CMAKE_SOURCE_DIR}/sync/server/src --plugin=protoc-gen-grpc=${CMAKE_SOURCE_DIR}/thirdparty/gRPC/vsprojects/x64/Debug/grpc_cpp_plugin.exe ${CMAKE_SOURCE_DIR}/protos/guide/hello.proto)
通过上面的命令,我们会生成四个文件:
hello.pb.*
中定义了 HelloReqeust
和 HelloResponse
消息的具体实现,而 hello.grpc.pb.*
中定义同步gRPC服务和异步gRPC服务等。后文的gRPC实现解析中会详细的讲解这块的代码。
class HelloService : public HelloSvc::Service { public: HelloService() = default; ~HelloService() = default; virtualStatussayHello(ServerContext* context,constHelloRequest* req, HelloResponse* rsp)override; }; Status HelloService::sayHello(ServerContext* context, const HelloRequest* req, HelloResponse* rsp) { std::cout << "Received from client: " << req->name() << std::endl; std::string response = "hello, "; rsp->set_message(response + req->name()); return Status::OK; } voidrunServer() { guide::HelloService service; ServerBuilder builder; builder.AddListeningPort("0.0.0.0:50051", grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr<Server> server(builder.BuildAndStart()); server->Wait(); }
sayHello
接口是在 HelloSvc::Service
类中定义,这个类就是在 hello.grpc.pb.h
中生成的同步服务类。 HelloService
服务实现类派生自该类,并实现 sayHello
接口,我们就可以利用 ServerBuilder
建立服务(绑定端口,)并运行。
客户端通过Stub来调用RPC服务端的代码,Stub必须运行在具体Channel上。我们必须要先建立信道:
grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials())
在建立信道的基础上,新建Stub,通过Stub来调用具体的RPC代码。
class HelloClient { public: HelloClient(std::shared_ptr<Channel> channel) : _stub(HelloSvc::NewStub(channel)) { } ~HelloClient() = default; std::stringsayHello(conststd::stringname); private: std::unique_ptr<HelloSvc::Stub> _stub; }; std::string HelloClient::sayHello(std::string user) { HelloRequest req; req.set_name(user); HelloResponse rsp; ClientContext ctx; Status status = _stub->sayHello(&ctx, req, &rsp); if (status.ok()) { return rsp.message(); } else { return "RPC Failed."; } }
调用RPC服务:
void runClient() { HelloClient client(grpc::CreateChannel( "localhost:50051", grpc::InsecureChannelCredentials())); std::string user("John"); std::string rsp = client.sayHello(user); std::cout << "Hello Client Received: " << rsp << std::endl; }
上文中我们给出了单向RPC示例,步骤二:生成RPC代码会生成RPC服务和客户端调用代码,这块代码是gRPC实现的核心代码。该段代码涉及到三点:
我们依次来看着三段代码:
1、客户端桩Stub类代码
class Stub GRPC_FINAL : public StubInterface { public: Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel); ::grpc::StatussayHello(::grpc::ClientContext* context,const::guide::HelloRequest& request, ::guide::HelloResponse* response)GRPC_OVERRIDE; std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::guide::HelloResponse>> AsyncsayHello(::grpc::ClientContext* context, const ::guide::HelloRequest& request, ::grpc::CompletionQueue* cq) { return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::guide::HelloResponse>>(AsyncsayHelloRaw(context, request, cq)); }
客户端桩Stub类中分别定义了同步版本和异步版本的RPC方法,我们可以按照我们的需求来选择。
2、服务端同步服务接口类代码
class Service : public ::grpc::Service { public: Service(); virtual ~Service(); virtual ::grpc::StatussayHello(::grpc::ServerContext* context,const::guide::HelloRequest* request, ::guide::HelloResponse* response); };
同步服务接口是阻塞的,服务端会阻塞在Server.wait()代码这儿,直到出现一次RPC调用。
3、服务端异步服务接口类代码
template <class BaseClass> class WithAsyncMethod_sayHello : public BaseClass { private: voidBaseClassMustBeDerivedFromService(constService *service){} public: WithAsyncMethod_sayHello() { ::grpc::Service::MarkMethodAsync(0); } ~WithAsyncMethod_sayHello() GRPC_OVERRIDE { BaseClassMustBeDerivedFromService(this); } voidRequestsayHello(::grpc::ServerContext* context, ::guide::HelloRequest* request, ::grpc::ServerAsyncResponseWriter< ::guide::HelloResponse>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq,void*tag) { ::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag); } }; typedef WithAsyncMethod_sayHello<Service > AsyncService;
异步服务接口是既可以是阻塞的也可以是非阻塞的,异步服务通过在 CompletionQueue
上等待完成实践,一旦等到相应的事件 Next
函数返回( AsyncNext
等到一定的时间间隔也会返回),执行相应的RPC服务代码。