复利效应
gRPC是Google开源的RPC框架,拥有高性能、跨语言等诸多优点。gRPC官方网站为grpc.io。鉴于官网的介绍较为混乱,并且其教程并不完善易懂,故而这里做一个简单的整理,希望一起成长。
本文所采用编程语言为C++,其他语言可以参考,开发平台为Windows平台。
如何使用gRPC
gRPC基于Protocol Buffer,在使用gRPC时,一般都是按照下列步骤:
- 定义proto3协议
- 生成RPC代码
- 实现服务端
- 实现客户端
上图是gRPC原理图,gRPC服务端实现具体的RPC服务,客户端通过gRPC Stub来调用这些RPC服务。客户端和服务端是通过信道(Channel
)来连接的。
gRPC有四种使用场景:单向RPC(一问一答)、服务端流式RPC(一问多答)、客户端流式RPC(多问一答)、双向流式RPC(多问多答)。gRPC的调用方式又分为同步(阻塞)和异步(非阻塞),所以我们需要根据需求,来选择使用场景和调用方式。四类服务方法如下:
- 单项RPC,客户端发送请求给服务端,服务端发送一个应答;
1
| rpc sayHello(HelloRequest) returns(HelloResponse) { }
|
- 服务端流式RPC,客户端发送请求给服务端,可获取一个数据流,通过该数据流能够读取服务端后续发送的一系列消息;
1
| rpc LotsOfReplies(HelloRequest) returns(stream HelloResponse) { }
|
- 客户端流式RPC,客户端用提供的一个数据流写入并发送一系列消息给服务端。
1
| rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) { }
|
- 双向流式RPC,客户端和服务端都可以分别通过一个读写数据流来发送一系列消息。
1
| rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){ }
|
对于gRPC使用场景,在生成RPC代码时,都会生成同步和异步接口。下文中会给出一个简单的同步单向gRPC示例,再次基础上我们会分析gRPC的详细代码。
有关gRPC的示例代码,都可以从Github: gRPC-Guide获取。
同步单向gRPC示例
定义proto3协议
这里需要有Protocol Buffer基础,具体使用可以Google。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| # hello.proto
syntax="proto3";
package guide;
message HelloRequest { string name = 1; }
message HelloResponse { string message = 1; }
service HelloSvc { rpc sayHello(HelloRequest) returns(HelloResponse); }
|
生成RPC代码
为了显示目录结构,下面的RPC代码借助了cmake。protoc.exe
可以从protobuf网站下载,也可以自己编译。
1 2 3 4 5 6 7 8 9
| 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实现解析中会详细的讲解这块的代码。
实现服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class HelloService : public HelloSvc::Service { public: HelloService() = default; ~HelloService() = default;
virtual Status sayHello(ServerContext* context, const HelloRequest* 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; }
void runServer() { 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上。我们必须要先建立信道:
1
| grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials())
|
在建立信道的基础上,新建Stub,通过Stub来调用具体的RPC代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class HelloClient { public: HelloClient(std::shared_ptr<Channel> channel) : _stub(HelloSvc::NewStub(channel)) { } ~HelloClient() = default;
std::string sayHello(const std::string name);
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服务:
1 2 3 4 5 6 7 8
| 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; }
|
gRPC实现解析
上文中我们给出了单向RPC示例,步骤二:生成RPC代码会生成RPC服务和客户端调用代码,这块代码是gRPC实现的核心代码。该段代码涉及到三点:
- 客户端桩Stub类代码
- 服务端同步服务接口类代码
- 服务端异步服务接口类代码
我们依次来看着三段代码:
1、客户端桩Stub类代码
1 2 3 4 5 6 7 8 9
| class Stub GRPC_FINAL : public StubInterface { public: Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel); ::grpc::Status sayHello(::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、服务端同步服务接口类代码
1 2 3 4 5 6
| class Service : public ::grpc::Service { public: Service(); virtual ~Service(); virtual ::grpc::Status sayHello(::grpc::ServerContext* context, const ::guide::HelloRequest* request, ::guide::HelloResponse* response); };
|
同步服务接口是阻塞的,服务端会阻塞在Server.wait()代码这儿,直到出现一次RPC调用。
3、服务端异步服务接口类代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| template <class BaseClass> class WithAsyncMethod_sayHello : public BaseClass { private: void BaseClassMustBeDerivedFromService(const Service *service) {} public: WithAsyncMethod_sayHello() { ::grpc::Service::MarkMethodAsync(0); } ~WithAsyncMethod_sayHello() GRPC_OVERRIDE { BaseClassMustBeDerivedFromService(this); } void RequestsayHello(::grpc::ServerContext* context, ::guide::HelloRequest* request, ::grpc::ServerAsyncResponseWriter< ::guide::HelloResponse>* reÂsponse, ::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服务代码。