复利效应
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服务代码。