简介
gRPC是Google开发的一个高性能、开源和通用的RPC框架,主要面向移动应用开发并基于HTTP/2协议标准而设计,使用Protobuf 3.0(Protocol Buffers) 序列化协议,支持多种编程语言。
学习本文前,应先学习Protocol Buffers。本文通过Windows平台和C++语言,记录开发常用笔记。
官方文档中文版:http://doc.oschina.net/grpc?t=57966
安装
打开PowerShell,通过vcpkg安装。注:会自动地同时安装Protocol Buffers。
.\vcpkg install grpc
安装完成后,在vcpkg安装目录下的tools\protobuf目录中会生成protobuf编译工具(protoc);在\tools\grpc目录中会生成一些gRPC编译语言插件。
定义gRPC服务
定义一个服务,你需要在.proto
文件中指定一个service
名称,例如:
service RouteGuide {
...
}
然后你可以在service
中定义rpc
方法,并制定它的request 和response 类型。在gRPC中你可以定义四种类型的服务方法:
- simple RPC – 简单 RPC ,客户端使用
stub
发送一个请求到服务端并等待响应返回,就像正常函数调用一样。// Obtains the feature at a given position. rpc GetFeature(Point) returns (Feature) {}
- server-side streaming RPC – 服务端流式 RPC,客户端向服务端发送请求,并获取流来读取返回的消息序列。客户机从返回的流读取,直到不再有消息为止。在示例中可以看到,通过在 response 类型之前放置
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) {}
- client-side streaming RPC – 客户端流式 RPC ,客户端使用给定的流多次写入消息序列并将其发送到服务端。一旦客户端完成了消息的写入,它就等待服务端读取所有消息并返回其响应。通过在 request 类型之前放置
stream
关键字来指定客户端流式方法。// Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {}
- bidirectional streaming RPC – 双向流式 RPC ,双方都使用 read-write 流发送消息序列。两流独立运行,所以客户端和服务端可以用任何顺序读取和写入:例如,服务端可以在接收完所有客户消息之前写入响应信息,或者它也可以交替读取信息然后写入响应信息,或者一些其他组合的读和写。每条流中的消息顺序是保持不变的。通过在 request 和 response 类型之前放置
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;
}
Hello World
1、在 .proto 文件中定义RPC服务
参考官方示例的HelloWorld工程,在工程下创建proto文件夹以便存放proto文件,然后编写Greeter.proto文件,例如D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto\Greeter.proto。
syntax = "proto3";
package TestGRPC;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
2、生成gRPC代码文件
通过Protocol Buffers编译器protoc.exe和gRPC C++插件(grpc_cpp_plugin.exe)来生成客户端和服务端代码。
执行:
protoc.exe -I="D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto" --cpp_out="D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto" "Greeter.proto"
在D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto目录下生成下列文件:
- Greeter.pb.h – Protobuf消息类的头文件
- Greeter.pb.cc – Protobuf消息类的实现
这些文件包含所有用来填充、序列化和检索我们的request和response消息类型的 protocol buffer 代码。
执行:
protoc.exe -I="D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto" --grpc_out="D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto" --plugin=protoc-gen-grpc="D:\SoftwareDevelopment\vcpkg\installed\x86-windows\tools\grpc\grpc_cpp_plugin.exe" "Greeter.proto"
在D:\SoftwareDevelopment\MFCApplication1\MFCApplication1\proto目录下生成下列文件:
- Greeter.grpc.pb.h – 服务类的头文件
- Greeter.grpc.pb.cc – 服务类的实现
这些文件包含服务端需要实现的两个接口类,以及service
中定义的客户端调用方法。由于Greeter.grpc.pb.h引用Greeter.pb.h,所以这些文件应放在同一文件夹中。
3、编写服务端代码
服务端代码需要做下面两部分工作:
- 基于
Service
定义实现Service
接口,也就是实现Service
的功能。 - 运行 gRPC 服务端侦听来自客户机的请求并返回
Service
响应。
示例,创建CTestGRPCServer类,继承Greeter::Service
。在Greeter.grpc.pb.h中能找到需要实现的SayHello
虚函数。
class CTestGRPCServer : public ::TestGRPC::Greeter::Service { protected: // 实现SayHello接口 virtual ::grpc::Status SayHello(::grpc::ServerContext* context, const ::TestGRPC::HelloRequest* request, ::TestGRPC::HelloReply* response); };
在CTestGRPCServer中实现SayHello
虚函数,例如。
Status CTestGRPCServer::SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) { std::string prefix("我是服务端,已收到你发送的请求:"); reply->set_message(prefix + request->name()); return Status::OK; }
在这里我们只实现 Greeter
的同步版本,它提供了默认的gRPC服务行为。当然你也可以使用Greeter::AsyncService
实现一个异步接口,这能让你进一步定制服务端的线程行为。
启动gRPC服务的步骤如下:
- 创建一个服务实现类的实例
CTestGRPCServer
; - 创建一个
ServerBuilder
工厂类的实例; - 使用
builder
对象的AddListeningPort()
方法指定监听客户端请求的地址和端口‘ - 使用
builder
对象的RegisterService()
方法注册服务实现; - 调用
builder
对象的BuildAndStart()
方法创建并启动 RPC 服务器,保存服务器指针; - 调用服务器指针的
Wait()
方法阻塞等待直到进程被关闭或直接调用服务器指针的Shutdown()
方法停止服务。
例如:
grpc::EnableDefaultHealthCheckService(true);
grpc::reflection::InitProtoReflectionServerBuilderPlugin();
CTestGRPCServer rpcServer = new CTestGRPCServer();
ServerBuilder builder;
std::string server_address("0.0.0.0:23351");
// Listen on the given address without any authentication mechanism.
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// Register "service" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *synchronous* service.
builder.RegisterService(_rpcServer);
// Finally assemble the server.
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
// Wait for the server to shutdown. Note that some other thread must be
// responsible for shutting down the server for this call to ever return.
server->Wait();
delete rpcServer;
std::cout << "Server stopped" << std::endl;
4、编写客户端代码
示例,创建CTestGRPCClient类,封装了Greeter::Stub
与grpc::Channel
,使用接收服务器地址的构造函数,并暴露出SayHello()
,如下:
class CTestGRPCClient
{
public:
CTestGRPCClient(std::string target_str);
std::string SayHello(const std::string& user);
private:
std::unique_ptr<Greeter::Stub> _stub;
std::shared_ptr<::grpc::Channel> _channel;
};
CTestGRPCClient::CTestGRPCClient(std::string target_str)
{
_channel = grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials());
_stub = Greeter::NewStub(_channel);
}
std::string CTestGRPCClient::SayHello(const std::string& user)
{
// Data we are sending to the server.
HelloRequest request;
request.set_name(user);
// Container for the data we expect from the server.
HelloReply reply;
// Context for the client. It could be used to convey extra information to
// the server and/or tweak certain RPC behaviors.
ClientContext context;
// The actual RPC.
Status status = _stub->SayHello(&context, request, &reply);
// Act upon its status.
if (status.ok())
{
return reply.message();
}
else
{
std::cout << status.error_code() << ": " << status.error_message() << std::endl;
throw "RPC failed";
}
}
使用示例:
std::string target_str = "localhost:23351";
CTestGRPCClient greeter(target_str);
std::string user("ClientA");
try
{
std::string reply = greeter.SayHello(user);
std::cout << "Greeter received: " << reply << std::endl;
}
catch (char* err)
{
cout << err << endl;
}
常见错误
1、warning STL4015: The std::iterator class template (used as a base class to provide typedefs) is deprecated in C++17.
1>D:\SoftwareDevelopment\vcpkg\installed\x64-windows\include\grpcpp\impl\codegen\security\auth_context.h(38,19): error C4996: ‘std::iterator<std::input_iterator_tag,const grpc::AuthProperty,ptrdiff_t,const grpc::AuthProperty *,const grpc::AuthProperty &>’: warning STL4015: The std::iterator class template (used as a base class to provide typedefs) is deprecated in C++17. (The <iterator> header is NOT deprecated.) The C++ Standard has never required user-defined iterators to derive from std::iterator. To fix this warning, stop deriving from std::iterator and start providing publicly accessible typedefs named iterator_category, value_type, difference_type, pointer, and reference. Note that value_type is required to be non-const, even for constant iterators. You can define _SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING or _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS to acknowledge that you have received this warning.
这个警告信息是因为grpc目前不支持C++17标准,但按照警告信息在项目配置中预定义_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING 或 _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS来解决。
2、warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失
1>D:\SoftwareDevelopment\gRPC_Study\gRPC_Example\proto\User\User.grpc.pb.h(1,1): warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode 格式以防止数据丢失
这是由于proto文件里包含中文等Unicode字符导致的,按提示将生成的代码文件再另存为Unicode编码就行了。
或者在编辑proto文件时,使用ANSI编码存储,这样生成的代码文件就不会出现这类问题了。
3、warning C4251: “google::protobuf::internal::LogMessage::message_”: class“std::basic_string,std::allocator>”需要有 dll 接口
禁用4251警告就行了。