前言:针对grpc 目前在C++ 开发过程中存在初学难懂,使用没有成体系说明的问题,这里对c++下的grpc 如何在代码层面使用进行简单说明。希望能够帮到更多人。
接口说明
本文后续使用代码均基于 下述的 proto文件,文件名为TestProto.proto
包含常见的使用示例
序列化规则文件
syntax = "proto3";
package TestProto;
// 这个服务类是请求响应的模式即单向流
service SvrPage{
// 数据传输监控
rpc GetTransferState(GetTransferStateRequest) returns (GetTransferStateResponse) {}
}
// 数据传输监控请求消息
message GetTransferStateRequest {
string dataType = 1; // 数据类型
string time = 2; // 数据日期(YYYY-MM-DD)
}
// 数据传输监控响应消息
message GetTransferStateResponse {
int32 code = 1; // 响应码
string msg = 2; // 响应消息
// 结构体数组
repeated GetTransferStateDataInfo data = 3; // 数据列表
}
// 数据信息
message GetTransferStateDataInfo {
// 基本类型数组
repeated int32 data = 1; // 数据记录
RecordMap mapData = 2; // 字典数据
}
//字段值
message FieldValueQc{
string code = 1; //编码
float value = 2; //数据值
int32 qcmark = 3; //质控码
bool lack = 4;//是否缺测
}
message RecordMap{
// 字典数据
map<string, FieldValueQc> line = 1; //key为code
}
// 监控服务 (这个服务是双向流,即异步发送请求应答)
service MonitorCenter {
rpc MonitorUpgradeState(stream UpgradeStateRequest) returns (stream UpgradeStateResponse);
}
message UpgradeStateRequest {}
message UpgradeStateResponse {
int32 code = 1;
string msg = 2;
UpgradeData data = 3;
}
message UpgradeData {
string state = 1;
repeated LogEntry lastedLog = 2;
}
message LogEntry {
string content = 1;
string time = 2;
}
服务端代码
服务端分两步:
- 初始化:
- 创建链接
- 挂起等待调用
- 远程调用实现
- 实现具体代码逻辑
也就是说:
- 如果你需要在服务端实现其他的初始化内容,请在挂起等待调用前。
- 如果你需要在远程调用中使用其他资源,请确保资源可用(公有资源,非抢占,过程中不存在挂起等待情况)
初始化
....
// 这里只对下面初始化需要使用的部分作说明,完整说明在远程调用实现部分
class CSvrPageImpl final : public SvrPage::Service
{
.....
};
// =============================================
// = 本端作为服务端 调用的服务区间 定义开始
// =============================================
/**
* @brief gRPC 作为服务端运行时的主体入口
*
*/
void RunServer()
{
// 这里实现指定绑定接口部分,接受string类型的对象传入参数
std::string server_address("0.0.0.0:50051");
// 这里初始化的类对象实例是在头文件中继承了`proto`文件中对应服务server 名称的类
CSvrPageImpl service;
// 固定 svr 创建方法
ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
// 至此,此进程进入挂起等待信息接入。
server->Wait();
}
int main ()
{
RunServer();
return 0;
}
远程调用实现
这里着重讲述如何实现具体的远程调用逻辑不涉及如何将程序运行起来,运行部分请参考上面的初始化部分。
#ifndef _OP_TEST_HPP_
#define _OP_TEST_HPP_
// 这里引用的头文件是在安装完成grpc服务后自动安装好的
#include <grpcpp/grpcpp.h>
// 这里的头文件是通过protobuffer程序生成的proto文件对应源码库
#include "../../include/proto/TestProto.grpc.pb.h"
#include "../../include/proto/TestProto.pb.h"
// 这里是服务端对应的必须内容
using grpc::Server;
using grpc::Status;
using grpc::ServerBuilder;
using grpc::ServerContext;
// 这里是根据proto文件定义的自己的message服务类
// 规则是:文件名:message
using TestProto::GetTransferStateRequest;
using TestProto::GetTransferStateResponse;
using TestProto::SvrPage;
using TestProto::GetTransferStateDataInfo;
using TestProto::RecordMap;
// 服务端类继承重载方法
class CSvrPageImpl final : public SvrPage::Service
{
// 接口定义,可以将定义声明分离,不必一定写成内嵌函数
// 返回值是grpc提供的自定义的状态类型,支持多种返回内容
grpc::Status GetTransferState(ServerContext *context, const GetTransferStateRequest *request, GetTransferStateResponse *response) override
{
// 实现获取传入的逻辑
std::string strDatatype = request->datatype();
std::string strtime = request->time();
std::cout << __LINE__ << __TIME__ << std::endl;
std::cout << "strDatatype: " << strDatatype << std::endl;
std::cout << "strtime: " << strtime << std::endl;
// 拼接返回值
response->set_code(200);
response->set_msg("success");
GetTransferStateDataInfo *sData = response->add_data();
sData->add_data(100); // 单项插入 数组尾
sData->add_data(101); // 单项插入 数组尾
sData->set_data(3,102); // 指定修改 数组 第三位的数据 为102
RecordMap *sRecordMap = sData->mutable_mapdata(); // 返回结构体对象指针,如果没有则创建
return grpc::Status::OK;
}
};
#endif // _OP_TEST_HPP_
客户端代码
客户端分三步是:
- 初始化
- 创建一个
gPRC
通道 - 调用接口
注:grpc支持返回原生的grpc状态码,作为远程调用的执行结果。 这是和grpc接口的返回值不同的概念。以此可以得到接口服务的调用结果和返回内容两项,从而实现更加宽泛的业务需求。
初始化
注意:为了保证最小依赖,相关的模版容器和字符串以及线程和其他引用的声明并未列出
#ifndef _OP_TEST_HPP_
#define _OP_TEST_HPP_
// 这里引用的头文件是在安装完成grpc服务后自动安装好的
#include <grpcpp/grpcpp.h>
// 这里的头文件是通过protobuffer程序生成的proto文件对应源码库
#include "../../include/proto/TestProto.grpc.pb.h"
#include "../../include/proto/TestProto.pb.h"
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
class CClientPageImpl
{
public:
CClientPageImpl(std::shared_ptr<Channel> channel)
: stub_(TestProto::NewStub(channel)) {}
// 调用远程函数
Status GetTransferState(const GetTransferStateRequest &request, GetTransferStateResponse &response);
private:
std::unique_ptr<DeviceService::Stub> stub_; // 通道
}
#endif // _OP_TEST_HPP_
创建
接口接收一个string类型对象作为绑定地址,另外这里主要对创建方式进行介绍,相关的前置依赖和类对象类型请参见初始化。
Status CClientPageImpl::GetTransferState(const std::string &strDatatype, const std::string &strtime)
int main()
{
// 创建一个gRPC通道
std::shared_ptr<Channel> channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
CClientPageImpl client(channel);
....
return 0;
}
调用接口
std::string CClientPageImpl::GetTransferState(const GetTransferStateRequest &request, GetTransferStateResponse &response)
{
ClientContext context;
Status status = stub_->SendRTRecordData(&context, request, &response);
// 对它的状态进行操作。
if (status.ok()) {
return std::to_string(response.data_size());
} else {
std::cout << __FILE__ << __LINE__ << std::endl;
std::cout << status.error_code() << ": " << status.error_message()
<< std::endl;
return "RPC 失败";
}
]
int main()
{
// 创建一个gRPC通道
std::shared_ptr<Channel> channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
CClientPageImpl client(channel);
ClientContext context;
GetTransferStateRequest request;
// 初始化传入接口
GetTransferStateResponse response;
std::string strRet = context.GetTransferState(request,response);
return 0;
}
序列化参数结构体读取 写入方式汇总
注意:grpc会对变量名称做优化,在使用Linux命名法、驼峰(大小驼峰)命名法时可能会出现相关函数和变量名称有些许差异的情况,请注意识别。
优化变量名排查方法:在grpc生成的对应源码文件中搜索 消息的类型定义名称,会在各个属性的类定义前有一行注释表明当前类是针对哪一个属性的。
读取方式
- 常量
常量是指proto文件中定义的对象类型是 数字、字符、这些基本类型
读取时,对应的结构体类中提供了同名的函数,返回值是对象对应值。
注意:grpc会对变量名称做优化,在使用Linux命名法、驼峰(大小驼峰)命名法时可能会出现相关函数和变量名称有些许差异的情况,请注意识别。
优化变量名排查方法:在grpc生成的对应源码文件中搜索 消息的类型定义名称,会在各个属性的类定义前有一行注释表明当前类是针对哪一个属性的。 - 结构体
结构体是指proto文件中定义的对象类型是嵌套结构体。即Struct 中套一个 struct。
读取时,对应的消息类中提供了同名的函数,返回值是结构体对象指针。 - 数组
数组是指proto文件中定义的对象类型 前又加了一个repeated
关键字 。常见于结构体数组。repeated int32 data = 1;
读取时,对应的消息类中提供了同名的函数,返回值是结构体对象的引用。 - 字典
字典是指proto文件中定义的对象类型 关键字为map
。常见于复杂结构。
读取时,对应的消息类中提供了同名的函数,返回值是字典对象的引用。
写入方式
由于上述内容已经对相关类型做了介绍,下面将主要以代码的方式进行说明,不再冗余介绍
- 常量
set_变量名() ;
- 结构体
mutable_结构体变量名();
- 数组
add_数组变量名();
- 字典
auto *p = mutable_字典名();
(*p) [键值] = value ;
编译常见问题
- 同名 变量冲突
- 结构体变量名和服务名
- 结构体变量名和命名空间名
- 服务名和命名空间名
- 多三方库同名冲突
- 避免connect,redis,sql,这些常见场景中的同名冲突,建议增加特征字符。
分享一个有趣的 学习链接:https://xxetb.xet.tech/s/HY8za