【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. 参考资料
- gRPC C++ 基础教程:服务定义与四种调用模式。
- 生成代码说明:Service、Stub 和方法接口。
- 取消机制、deadline。
- 完整构建与运行入口:协议生成、服务端、客户端。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/xusiwei1236/article/details/166600144




