gRPC 入门:原理与用法一图景
在现代 C++ / 后端系统里,gRPC 几乎是默认的“跨进程 RPC 协议栈”之一。
它的目标是:高性能、跨语言、强类型的远程调用。
从抽象视角看,你可以把 gRPC 理解为三层叠加:
- IDL + 代码生成:用
.proto文件定义消息类型与服务接口,gRPC 工具链为各语言生成 stub 代码; - 传输协议:基于 HTTP/2 的二进制帧、流式传输、压缩等能力;
- 运行时库:封装连接管理、负载均衡、重试、deadline、metadata 等常见 RPC 需求。
一、gRPC 的核心理念:IDL 驱动的远程调用
在没有 gRPC 的时代,一个服务调用另一个服务通常要自己解决:
- 请求/响应的序列化格式(JSON?自定义二进制?)
- HTTP 方法和 URL 的约定
- 错误码和重试语义
- 客户端 SDK 的封装与维护
gRPC 的做法是:把这些约定固化进一套统一的模型,让你用一个 .proto 文件说清楚:
- 这个服务里有哪些 RPC 方法;
- 每个方法的请求消息、响应消息长什么样;
- 是一问一答的 unary,还是流式的 streaming。
然后它用代码生成器帮你在各个语言里生成:
- 服务端接口(你只实现业务逻辑);
- 客户端 stub(像本地函数一样发起远程调用)。
二、协议栈:gRPC 究竟跑在什么之上?
gRPC 的典型协议栈可以简化为:
应用层(你的业务)
↓
gRPC Runtime(C++ / Go / Java / … 实现)
↓
HTTP/2(二进制帧、流、多路复用)
↓
TLS(可选)
↓
TCP
其中几件事情非常关键:
- HTTP/2 多路复用:同一 TCP 连接上可以并发多个独立的 stream(每个 stream 就是一条 RPC),避免了 HTTP/1.1 的队头阻塞问题;
- 二进制帧:相比文本协议更易于高效解析和压缩;
- ProtoBuf 序列化:消息体通常使用 Protocol Buffers,既紧凑又有强类型描述。
这意味着:从性能和带宽利用率上,gRPC 通常优于传统的 REST+JSON 方案,尤其是在高并发场景下。
三、Proto 文件:先定义“合同”,再写代码
使用 gRPC 的第一步,不是写 C++ 代码,而是写一个 .proto 文件。
它同时承担了三种角色:
- 类型系统(定义消息结构);
- 接口定义(定义服务与方法);
- 跨语言协议(所有语言共用这一份说明书)。
一个最小的例子大致如下(简化示意):
syntax = "proto3";
package example;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
定义完 .proto 后,你会用 protoc + gRPC 插件生成 C++ 代码:
xxx.grpc.pb.h/xxx.grpc.pb.cc:gRPC 服务与客户端 stub;xxx.pb.h/xxx.pb.cc:普通的 ProtoBuf 消息类型。
四、服务端:实现接口,而不是处理 socket
在 C++ 侧,生成的 gRPC 代码通常会给你一个抽象基类,类似(伪代码):
class Greeter::Service {
public:
virtual Status SayHello(ServerContext* ctx,
const HelloRequest* req,
HelloReply* resp);
};
你要做的事情只是:
- 继承这个基类;
- 实现每个 RPC 方法的业务逻辑(读取请求、填充响应、返回状态)。
至于:
- 如何监听端口;
- 如何接收/解析请求;
- 如何在 HTTP/2 stream 里调度;
- 如何处理连接的生命周期;
这些都由 gRPC runtime 帮你完成。你更像是在“填表”,告诉 gRPC:
- 对于这个方法,收到了消息以后我要做什么。
五、客户端:像本地函数一样调用远程服务
在同一个 .proto 基础上,gRPC 也会为客户端生成 stub 类。
在 C++ 侧,通常长这样(伪代码):
std::unique_ptr<Greeter::Stub> stub =
Greeter::NewStub(grpc::CreateChannel("server:port", creds));
HelloRequest req;
req.set_name("world");
HelloReply resp;
ClientContext ctx;
Status s = stub->SayHello(&ctx, req, &resp);
在你的 C++ 代码里,这看起来就像一次普通函数调用:
- 你构造请求对象;
- 调用
stub->Method(...); - 拿到响应对象和一个
Status。
底层的连接管理、序列化、重试(若你配置了)、deadline 等,都被封装在 gRPC runtime 内部。
六、gRPC 的调用模式:不止一问一答
gRPC 支持四种调用模式,它们都基于 HTTP/2 的 stream。
-
Unary RPC(一问一答)
- 最常见的模式:一个请求,一个响应;
- 对使用者来说很接近传统函数调用。
-
Server streaming
- 客户端发一次请求,服务器返回一个“响应流”;
- 适合订阅型场景:例如订阅行情、日志推送、数据流。
-
Client streaming
- 客户端发送一个“请求流”,服务器在结束时返回一个总结响应;
- 适合上传批量数据,例如批量上报 metrics。
-
Bidirectional streaming
- 双向都是流;客户端和服务器都可以在同一条连接上持续读写;
- 适合聊天、交互式会话、复杂协调协议。
在 C++ API 中,这些模式分别对应不同形态的 stub 方法和 Reader / Writer / ReaderWriter 对象。
七、重要运行时概念:deadline、metadata、状态码
一些 gRPC 的运行时概念,在工程里很关键:
-
Deadline / Timeout
- 每个 RPC 都可以设置 deadline(如 200ms 内必须返回);
- gRPC 会在超时后主动取消请求,并返回相应状态;
- 这是构建“可预期的延迟上界”和“端到端超时控制”的基础。
-
Metadata
- 类似 HTTP 头,可以随请求/响应携带额外键值对;
- 常用于传输认证信息、trace id、租户信息等。
-
Status / 错误码
- gRPC 自带一套标准错误码(OK、NOT_FOUND、UNAVAILABLE、DEADLINE_EXCEEDED 等);
- 你可以在 Status 中附加人类可读的信息;
- 工程里通常会把“业务错误码”二次封装在 payload 或 metadata 里。
这些机制一起构成了“RPC 语义层”的合同,让你能从 C++ 代码里可靠地观察远程调用是否成功、失败、超时、被取消。
八、在 C++ 项目里使用 gRPC 的典型步骤
综合来看,一个 C++ 项目如果要引入 gRPC,大致会经历这些步骤:
-
设计服务与消息模型
- 用
.proto文件描述 service / rpc / message; - 在这个阶段就要考虑版本演进(字段号、可选字段、默认值)。
- 用
-
生成代码
- 在构建系统(CMake 等)里集成
protoc+grpc_cpp_plugin; - 生成
*.pb.cc/*.grpc.pb.cc并编译进工程。
- 在构建系统(CMake 等)里集成
-
实现服务端逻辑
- 继承生成的
Service基类; - 实现每个方法的业务逻辑;
- 配置并启动 gRPC 服务器(线程模型、端口、凭证等)。
- 继承生成的
-
实现客户端调用
- 创建
Channel(含负载均衡/证书配置); - 构造 stub,像调用本地函数一样发起 RPC;
- 合理设置 deadline、重试策略和错误处理。
- 创建
-
运维与观测
- 在 metadata 里透传 trace id / span id;
- 利用 gRPC 自带的统计接口或在中间层打点;
- 结合负载均衡、熔断、限流构建完整的 service mesh(常见搭档:Envoy、Istio 等)。
九、小结:什么时候用 gRPC,什么时候不用?
适合用 gRPC 的情况:
- 服务间调用频繁、对性能和延迟敏感;
- 需要强类型、跨语言、统一治理(负载均衡、重试、超时)的 RPC;
- 服务拓扑稳定,有专门的服务发现/配置中心;
- 整体技术栈已经有 gRPC 生态(监控、trace、sidecar 等)。
可以不必上 gRPC 的情况:
- 对性能要求不高,更倾向于开放/通用的 HTTP+JSON API;
- 主要面向浏览器或不易使用 HTTP/2 的客户端;
- 场景非常简单,不值得引入额外的运行时与生成工具链。
理解 gRPC 的最好方式,是同时带着“协议栈视角”和“编译器视角”看它:
- 从协议栈看:它是跑在 HTTP/2 + ProtoBuf 上的一套 RPC 语义;
- 从编译器视角看:
.proto就是跨语言的“类型和接口合同”,生成的 C++ 代码只是这份合同的一个具体实现。