码工许师傅头像
关注
【C++三方组件】gRPC:现代微服务 RPC封面图

【C++三方组件】gRPC:现代微服务 RPC

【C++三方组件】gRPC:现代微服务 RPC

【摘要】:gRPC 用接口定义和代码生成组织跨进程调用,提供一元调用、流式传输、状态码、deadline 与取消等机制。本文说明它替应用承担了哪些 RPC 基础工作,再通过一个完整 Echo 服务,演示从 .proto、构建到四种调用模式的使用过程。
【版本基准】:gRPC 1.82.0(Apache-2.0)+protobuf 35.1(BSD-3-Clause)|C++17。完整的 协议定义、服务端、客户端和构建配置均随文提供。

1. What:gRPC 是什么

gRPC 是一个跨语言 RPC 框架。典型使用方式是在 .proto 中定义消息和服务,通过 protoc 及 gRPC 插件生成消息类、客户端代理和服务端接口,再由运行时处理调用的传输与生命周期。

在常见的 protobuf 使用方式下,可以把它分成三层:

.proto:消息结构、服务、方法和流方向
    ↓ 代码生成
客户端 Stub ←→ gRPC 运行时与 HTTP/2 ←→ 服务端 Service 实现

RPC 保留了“调用一个方法、取得结果”的使用体验,但远程调用与本地函数有不同的失败条件:网络会断、服务可能不可用、客户端可能已超时而业务仍在执行。gRPC 提供表达这些状态的接口,不会让分布式失败自动消失。

2. Why:为什么要用 RPC 框架

用 TCP 加 protobuf 可以实现最小请求应答;用 HTTP 加 JSON 也可以建设服务接口。随着服务、语言和调用数量增加,基础工作会逐渐超出“把字节发过去”:

需求自行实现或维护的部分gRPC 提供的机制
客户端调用远程方法方法标识、消息编解码、请求与结果关联服务定义、Stub、生成代码
多语言共同使用接口各语言消息模型、客户端与接口约定IDL 与多语言生成器
复用连接处理多个请求多路复用、流管理、连接状态Channel 与 HTTP/2 传输
一次调用返回多条消息消息边界、流方向与结束状态四种 RPC 方法形态
限制调用等待时间截止时间、错误分类、取消通知deadline、Status、取消 API
传递认证或追踪信息约定附加字段及调用上下文metadata 与扩展接口

这些机制让团队能够共享一套调用约定,减少客户端和服务端各自维护协议代码的成本。代价是增加代码生成、运行时依赖和接口治理工作,调试二进制消息也需要相应工具。

如果主要面对浏览器和公开 HTTP API,REST/OpenAPI 可能更便于使用;若服务间需要强接口契约、多语言生成代码或流式调用,gRPC 值得考虑。是否采用应结合已有平台与团队工具,而不是默认所有服务都需要 RPC 框架。

3. How:从协议定义到可运行服务

配套案例提供一个 Echo 服务。服务端和客户端是两个独立可执行文件,便于理解实际部署边界;本地练习使用回环地址与明文凭据。

3.1 定义消息与四种方法

echo.proto 内容如下:

syntax = "proto3";
package echo;

message Msg { string text = 1; }
message RepeatRequest { string text = 1; int32 count = 2; }
message Summary { int32 count = 1; }

service Echo {
  rpc Shout(Msg) returns (Msg);
  rpc Repeat(RepeatRequest) returns (stream Msg);
  rpc Collect(stream Msg) returns (Summary);
  rpc Chat(stream Msg) returns (stream Msg);
  rpc Slow(Msg) returns (Msg);
}

stream 出现在参数侧表示客户端发送消息流,出现在返回侧表示服务端发送消息流:

方法形态本例用途可类比的业务
Shout一元一条输入、一条输出查询、提交命令
Repeat服务端流按次生成多条消息推送进度、日志订阅
Collect客户端流接收多条消息后统计上传、批量聚合
Chat双向流在同一调用中多轮收发会话、控制通道

消息字段号是协议的一部分。删除字段时应保留其编号,避免后来赋予另一个含义;修改方法的流形态也属于接口变化,不能因为消息字段没变就认为旧客户端仍然兼容。

3.2 生成代码与 CMake 集成

vcpkg 依赖为 grpc、protobuf。生成阶段使用 protoc 和 grpc_cpp_plugin:

find_package(Protobuf CONFIG REQUIRED)
find_package(gRPC CONFIG REQUIRED)

set(generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
file(MAKE_DIRECTORY "${generated_dir}")
add_custom_command(
  OUTPUT "${generated_dir}/echo.pb.cc" "${generated_dir}/echo.pb.h"
         "${generated_dir}/echo.grpc.pb.cc" "${generated_dir}/echo.grpc.pb.h"
  COMMAND protobuf::protoc
    "--proto_path=${CMAKE_CURRENT_SOURCE_DIR}"
    "--cpp_out=${generated_dir}" "--grpc_out=${generated_dir}"
    "--plugin=protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>"
    "${CMAKE_CURRENT_SOURCE_DIR}/echo.proto"
  DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/echo.proto"
          protobuf::protoc gRPC::grpc_cpp_plugin
  VERBATIM)

echo.pb.* 是消息代码,echo.grpc.pb.* 是服务接口和客户端代理。完整 CMake 把它们组成 echo_proto 目标,再由服务端和客户端链接。

通过导入目标定位插件,可以避免手写 Windows 路径与 .exe 后缀;VERBATIM 负责命令参数转义。生成器和运行时应使用兼容版本,跨平台交叉编译时还应区分宿主可执行的生成工具与目标平台库。

在配套工程中选择 BLOG_COMPONENTS=grpc,构建步骤见 README。

3.3 服务端:实现生成的 Service

服务端继承 echo::Echo::Service,重写方法。最简单的一元调用如下:

grpc::Status Shout(grpc::ServerContext* context,
                   const echo::Msg* request, echo::Msg* reply) override {
  std::string text = request->text();
  std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
    return static_cast<char>(std::toupper(c));
  });
  reply->set_text(text);
  return grpc::Status::OK;
}

这个转换适用于本例英文输入,不是通用 Unicode 大小写转换。业务失败时返回明确的 Status;例如 Repeat 的 count 超出 0~100 时,示例返回 INVALID_ARGUMENT。

注册服务并启动:

EchoService service;
grpc::ServerBuilder builder;
int port = 0;
builder.AddListeningPort("127.0.0.1:18083",
                         grpc::InsecureServerCredentials(), &port);
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
if (!server || port == 0) return 1;
server->Wait();

Service 对象必须覆盖服务的使用期。这里使用同步服务端接口,方法可以在不同工作线程上执行;若方法访问共享业务状态,需要相应同步。本文没有用同步接口的简洁性来推断它适合任意负载。

3.4 一元调用与 metadata

客户端首先创建 Channel 和 Stub:

auto channel = grpc::CreateChannel("127.0.0.1:18083",
                                    grpc::InsecureChannelCredentials());
auto stub = echo::Echo::NewStub(channel);
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(3));
context.AddMetadata("x-trace-id", "demo-001");
echo::Msg request, reply;
request.set_text("hello, grpc");
auto status = stub->Shout(&context, request, &reply);
if (!status.ok()) throw std::runtime_error(status.error_message());

Channel 是逻辑通信通道,可能涉及名称解析、负载均衡与多个底层连接,不应简单等同于一条永不变化的 TCP 连接。通常可以复用 Channel 和 Stub;每次 RPC 使用新的 ClientContext。

完整服务端读取 x-trace-id 并在 trailing metadata 中返回。客户端在调用完成后通过 GetServerTrailingMetadata 读取它。metadata 适合调用级追踪或认证信息,实际身份与权限仍需业务验证。

3.5 服务端流:Read 结束后还要 Finish

服务端根据 count 循环 writer->Write(message),检查返回值后继续;客户端逐条读取:

grpc::ClientContext context;
deadline(context);
echo::RepeatRequest request;
request.set_text("tick");
request.set_count(3);
auto reader = stub->Repeat(&context, request);
echo::Msg message;
while (reader->Read(&message))
  std::cout << "stream=" << message.text() << '\n';
check(reader->Finish());

这里的 deadline 和 check 是配套客户端的辅助函数。Read 返回 false 说明没有下一条消息,但不能单独证明成功:正常结束、取消、错误都可能终止读取,最终状态由 Finish 给出。

三次 Write 产生三条应用消息;它们属于同一次流式 RPC,不应解释成三个独立 HTTP/2 stream,也不应假设一条消息必然对应一个 HTTP/2 DATA 帧。

3.6 客户端流:WritesDone 表示不再发送

grpc::ClientContext context;
deadline(context);
echo::Summary summary;
auto writer = stub->Collect(&context, &summary);
for (int i = 0; i < 3; ++i)
  if (!writer->Write(request)) break;
writer->WritesDone();
check(writer->Finish());
std::cout << "collected=" << summary.count() << '\n';

服务端通过 ServerReader 读取到输入结束,再填写最终响应。客户端在写完后调用 WritesDone,相当于结束发送方向;随后仍需取得 RPC 的最终状态。

这个模式适合逐步上传或聚合,但还要限制总消息数量和单条大小。配套服务端为演示设置了消息数上限。

3.7 双向流:协议决定收发节奏

配套 Chat 使用简单的“一发一收”协议:

grpc::ClientContext context;
deadline(context);
auto chat = stub->Chat(&context);
for (int i = 1; i <= 2; ++i) {
  request.set_text("chat " + std::to_string(i));
  if (!chat->Write(request) || !chat->Read(&reply)) {
    context.TryCancel();
    check(chat->Finish());
    throw std::runtime_error("chat ended early");
  }
  std::cout << "chat=" << reply.text() << '\n';
}
chat->WritesDone();
while (chat->Read(&reply)) {}
check(chat->Finish());

双向流允许两端独立组织发送与接收,本例的交替节奏是应用协议的选择。实际聊天或持续推送可能要分别驱动读写,并遵守相应 API 对并发读写的约定,避免两端都等待对方先发送。

3.8 deadline 与取消:给每次调用时间预算

配套客户端给 Slow 设置 100ms deadline,服务端的工作分成多轮,每轮检查取消:

for (int i = 0; i < 30; ++i) {
  if (context->IsCancelled())
    return {grpc::StatusCode::CANCELLED, "stopped early"};
  std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

客户端 deadline 到期后返回 DEADLINE_EXCEEDED。服务器是否尽快停止业务工作,取决于业务是否检查取消、是否能中断下游操作;框架不会强行终止任意 C++ 函数。

如果服务端还要调用另一个服务,C++ 中要显式建立 deadline/取消传播关系,例如通过 ClientContext::FromServerContext 创建下游 context,并按需要设置传播选项。只在入口设置 deadline,并不证明任意新建的下游 ClientContext 都会继承它。官方 deadline 说明

3.9 运行完整案例

在终端 A 运行服务端,在终端 B 运行客户端:

# 终端 A
./build/grpc_server 127.0.0.1:18083
# 终端 B
./build/grpc_client 127.0.0.1:18083

客户端包含有限时长的连接就绪等待,每个 RPC 也设置 deadline。成功运行会验证四种模式、metadata、超时和参数错误,并输出:

unary=HELLO, GRPC
trace=demo-001
stream=tick #1
stream=tick #2
stream=tick #3
collected=3
chat=echo: chat 1
chat=echo: chat 2
deadline_code=4
invalid_code=3

4. 从示例到项目

HTTP/2 的多路复用与流控便于并发 RPC,但同时活动的流受协商和资源限制,也没有消除底层 TCP 丢包造成的阻塞。部署时还要确认代理或负载均衡是否正确支持 gRPC,不能只检查“支持 HTTP”就结束。

本例的明文凭据用于本地练习。跨不可信网络应配置适当的 TLS、身份认证和授权;长连接保活参数应与服务端策略协调,而不是任意缩短间隔。超时重试还需结合幂等性判断,因为客户端超时不保证服务端完全没有产生副作用。

同步 API 便于入门,进一步可以学习回调 API 或 CompletionQueue。选择应由并发模型、阻塞工作量和团队维护成本决定。

5. 参考资料

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/xusiwei1236/article/details/166600144

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--