gRPC-开发常用笔记

简介

gRPC是Google开发的一个高性能、开源和通用的RPC框架,主要面向移动应用开发并基于HTTP/2协议标准而设计,使用Protobuf 3.0(Protocol Buffers) 序列化协议,支持多种编程语言。

学习本文前,应先学习Protocol Buffers。本文通过Windows平台和C++语言,记录开发常用笔记。

官网:https://grpc.io/

官方文档中文版:http://doc.oschina.net/grpc?t=57966

安装

打开PowerShell,通过vcpkg安装。注:会自动地同时安装Protocol Buffers。

.\vcpkg install grpc

安装完成后,在vcpkg安装目录下的tools\protobuf目录中会生成protobuf编译工具(protoc);在\tools\grpc目录中会生成一些gRPC编译语言插件。

proco程序
gRPC编译语言插件

定义gRPC服务

定义一个服务,你需要在.proto文件中指定一个service 名称,例如:

service RouteGuide {
   ...
}

然后你可以在service中定义rpc 方法,并制定它的requestresponse 类型。在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服务的步骤如下:

  1. 创建一个服务实现类的实例 CTestGRPCServer ;
  2. 创建一个 ServerBuilder 工厂类的实例;
  3. 使用 builder 对象的AddListeningPort() 方法指定监听客户端请求的地址和端口‘
  4. 使用 builder 对象的RegisterService()方法注册服务实现;
  5. 调用 builder 对象的BuildAndStart() 方法创建并启动 RPC 服务器,保存服务器指针;
  6. 调用服务器指针的 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::Stubgrpc::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编码存储,这样生成的代码文件就不会出现这类问题了。

使用Notepad++转换

3、warning C4251: “google::protobuf::internal::LogMessage::message_”: class“std::basic_string,std::allocator>”需要有 dll 接口

禁用4251警告就行了。

参考文章

1、《在Windows系统上编译及使用gRPC》作者:永无止境

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注