gRPC C++ Callback API(Reactor 模式)介绍
本文面向想理解 gRPC C++ Callback API 的开发者,重点解释它的设计思想、核心接口、执行流程、时序关系、常见写法,以及使用时的注意事项。
目录
- [1. 什么是 gRPC C++ Callback API](#1. 什么是 gRPC C++ Callback API)
- [2. 什么是 Reactor 模式](#2. 什么是 Reactor 模式)
- [3. Callback API 解决了什么问题](#3. Callback API 解决了什么问题)
- [4. Callback API 的核心思想](#4. Callback API 的核心思想)
- [5. 服务端 Reactor 类型](#5. 服务端 Reactor 类型)
- [6. 客户端 Reactor 类型](#6. 客户端 Reactor 类型)
- [7. 核心回调方法](#7. 核心回调方法)
- [8. 核心启动方法](#8. 核心启动方法)
- [9. Unary RPC 示例](#9. Unary RPC 示例)
- [10. Bidirectional Streaming 示例](#10. Bidirectional Streaming 示例)
- [11. Callback API 的时序图](#11. Callback API 的时序图)
- [12. 把 Reactor 理解成状态机](#12. 把 Reactor 理解成状态机)
- [13. 与同步 API、CompletionQueue API 对比](#13. 与同步 API、CompletionQueue API 对比)
- [14. 生命周期与资源管理](#14. 生命周期与资源管理)
- [15. 并发与线程安全注意事项](#15. 并发与线程安全注意事项)
- [16. 常见坑与最佳实践](#16. 常见坑与最佳实践)
- [17. 适用场景](#17. 适用场景)
- [18. 不一定适合的场景](#18. 不一定适合的场景)
- [19. 总结](#19. 总结)
1. 什么是 gRPC C++ Callback API
gRPC C++ 提供多种编程模型:
- 同步 API
- 异步 API(CompletionQueue)
- Callback API
其中,Callback API 是一种更偏现代事件驱动风格的接口。它的核心特点是:
- 你不需要自己维护
CompletionQueue - 你不需要自己写复杂的事件轮询循环
- 你只需要实现一组"事件回调"方法
- 当读、写、结束、取消等事件发生时,gRPC 会回调你的对象
在 gRPC C++ 中,这种写法通常通过 Reactor 来表达,所以也常被称为:
- gRPC C++ Callback API
- gRPC C++ Reactor API
- 基于 Reactor 模式的 Callback API
2. 什么是 Reactor 模式
从设计模式角度看,Reactor 模式 是一种事件驱动模型:
- 底层框架负责监听事件
- 当事件发生时,分发给对应的处理器
- 处理器根据事件执行逻辑并推进状态
在 gRPC C++ Callback API 中:
- gRPC runtime 负责底层网络 IO 和事件分发
- 你的 Reactor 对象 负责处理回调事件
- 回调事件通常包括:
- 读完成
- 写完成
- RPC 被取消
- RPC 整体结束
所以可以把它理解为:
gRPC 帮你管异步事件循环,你只需要在事件发生时告诉它"下一步怎么做"。
3. Callback API 解决了什么问题
3.1 相比同步 API
同步 API 的优点是简单直观,但流式处理和高并发场景下,表达能力有限,线程占用也可能更明显。
3.2 相比 CompletionQueue API
CompletionQueue API 很强大,但也更底层:
- 需要自己拉取事件
- 需要自己管理 tag
- 需要自己分发事件
- 需要自己维护状态机
这会带来:
- 代码模板较多
- 状态管理复杂
- 维护成本高
3.3 Callback API 的定位
Callback API 正好位于二者之间:
- 保留异步能力
- 比 CompletionQueue 更易写
- 非常适合事件驱动和流式 RPC
一句话概括:
CompletionQueue 模式是"我自己管理异步事件";Callback 模式是"事件到了你回调我"。
4. Callback API 的核心思想
Callback API 的基本思路不是写"从上到下"的阻塞式逻辑,而是写成:
- 创建一个 Reactor
- 发起第一个异步动作
- 等待 gRPC 回调
- 在回调里决定下一步
- 直到最终结束 RPC
也就是说,代码形态从:
text
read -> process -> write -> finish
变成:
text
StartRead(...)
OnReadDone(...) {
process
StartWrite(...)
}
OnWriteDone(...) {
StartRead(...) / Finish(...)
}
OnDone() {
cleanup
}
所以 Callback API 本质上是:
- 事件驱动
- 回调驱动
- 状态机驱动
5. 服务端 Reactor 类型
服务端常见的 Reactor 基类有以下几种。
5.1 Unary RPC
单请求、单响应。
对应基类:
cpp
grpc::ServerUnaryReactor
适用的 proto 形式:
proto
rpc SayHello(HelloRequest) returns (HelloReply);
5.2 Server Streaming
客户端发一个请求,服务端返回多条消息。
对应基类:
cpp
grpc::ServerWriteReactor<Response>
适用的 proto 形式:
proto
rpc ListItems(ListRequest) returns (stream Item);
5.3 Client Streaming
客户端发送多条消息,服务端最终返回一个响应。
对应基类:
cpp
grpc::ServerReadReactor<Request>
适用的 proto 形式:
proto
rpc Upload(stream Chunk) returns (UploadReply);
5.4 Bidirectional Streaming
客户端和服务端都可以持续发送消息。
对应基类:
cpp
grpc::ServerBidiReactor<Request, Response>
适用的 proto 形式:
proto
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
6. 客户端 Reactor 类型
客户端也有对应的 Callback/Reactor 风格 API。
常见基类有:
grpc::ClientUnaryReactorgrpc::ClientReadReactor<T>grpc::ClientWriteReactor<T>grpc::ClientBidiReactor<Req, Resp>
其思想与服务端完全一致:
- 发起操作
- 等待回调
- 在回调中推进流程
- 最终结束
7. 核心回调方法
不同 Reactor 类型支持的方法略有差异,但最常见的是以下几个。
7.1 OnReadDone(bool ok)
表示一次 StartRead() 操作已经完成。
cpp
void OnReadDone(bool ok) override;
语义
ok == true- 成功读到一条消息
- 对应的消息对象里已有完整数据
ok == false- 没有更多消息可读
- 对于流式场景,通常表示对端关闭了写方向或流结束
常见用途
- 处理刚收到的消息
- 决定是否继续
StartRead - 决定是否发起
StartWrite - 决定是否
Finish
7.2 OnWriteDone(bool ok)
表示一次 StartWrite() 操作已经完成。
cpp
void OnWriteDone(bool ok) override;
语义
ok == true- 当前消息成功写出
ok == false- 后续一般无法继续写
- 常见于流已终止、连接异常、被取消等情况
常见用途
- 继续写下一条消息
- 写完后发起下一次读
- 无法继续时调用
Finish
7.3 OnDone()
表示整个 RPC 已经彻底结束。
cpp
void OnDone() override;
这是整个 RPC 生命周期的最终收尾点。
常见用途
- 释放 Reactor 对象
- 清理资源
- 打印日志
- 统计指标
常见写法:
cpp
void OnDone() override {
delete this;
}
7.4 OnCancel()
表示 RPC 被取消。
cpp
void OnCancel() override;
取消原因可能包括:
- 客户端主动取消
- 超时
- 连接断开
- 上层逻辑中止
常见用途
- 停止后台任务
- 标记取消状态
- 避免继续发消息
- 记录异常退出日志
8. 核心启动方法
8.1 StartRead(&msg)
发起一次异步读操作。
cpp
StartRead(&req_);
含义是:
请 gRPC 异步读取下一条输入消息,读完以后回调
OnReadDone()。
注意
- 消息对象必须在回调发生前一直有效
- 通常一次只能有一个读操作在进行中
8.2 StartWrite(&msg)
发起一次异步写操作。
cpp
StartWrite(&resp_);
含义是:
请 gRPC 异步发送这条输出消息,写完以后回调
OnWriteDone()。
注意
- 消息对象必须在写完成前一直有效
- 通常一次只能有一个写操作在进行中
8.3 Finish(status)
结束 RPC。
cpp
Finish(grpc::Status::OK);
或返回错误:
cpp
Finish(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "bad request"));
注意
Finish()只是"发起结束"- 真正完全结束是在
OnDone()中体现
9. Unary RPC 示例
Unary RPC 最简单,因为没有持续的读写循环。
9.1 服务定义示例
proto
service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply);
}
9.2 服务端写法
cpp
class HelloReactor : public grpc::ServerUnaryReactor {
public:
HelloReactor(const HelloRequest* req, HelloReply* resp) {
resp->set_message("Hello " + req->name());
Finish(grpc::Status::OK);
}
void OnDone() override {
delete this;
}
};
9.3 服务方法
cpp
class GreeterServiceImpl final : public Greeter::CallbackService {
public:
grpc::ServerUnaryReactor* SayHello(
grpc::CallbackServerContext* context,
const HelloRequest* request,
HelloReply* response) override {
return new HelloReactor(request, response);
}
};
9.4 执行流程
- gRPC 收到请求
- 调用服务方法,创建 Reactor
- Reactor 填充响应
- 调用
Finish(Status::OK) - RPC 完成后调用
OnDone()
10. Bidirectional Streaming 示例
双向流最能体现 Reactor 模式。
10.1 proto 示例
proto
service ChatService {
rpc Chat(stream ChatReq) returns (stream ChatResp);
}
10.2 服务端示例
cpp
class ChatReactor : public grpc::ServerBidiReactor<ChatReq, ChatResp> {
public:
ChatReactor() {
StartRead(&req_);
}
void OnReadDone(bool ok) override {
if (!ok) {
Finish(grpc::Status::OK);
return;
}
resp_.set_text("echo: " + req_.text());
StartWrite(&resp_);
}
void OnWriteDone(bool ok) override {
if (!ok) {
Finish(grpc::Status::OK);
return;
}
StartRead(&req_);
}
void OnCancel() override {
// 可选:处理取消逻辑
}
void OnDone() override {
delete this;
}
private:
ChatReq req_;
ChatResp resp_;
};
10.3 服务实现
cpp
class ChatServiceImpl final : public ChatService::CallbackService {
public:
grpc::ServerBidiReactor<ChatReq, ChatResp>* Chat(
grpc::CallbackServerContext* context) override {
return new ChatReactor();
}
};
10.4 这段代码的逻辑
- Reactor 创建后,先调用
StartRead(&req_) - 每收到一条客户端消息,进入
OnReadDone(true) - 处理后调用
StartWrite(&resp_) - 写完成后进入
OnWriteDone(true) - 再发起下一次
StartRead(&req_) - 如果读到
ok == false,说明客户端没有更多消息,调用Finish(Status::OK) - 真正结束后调用
OnDone()
11. Callback API 的时序图
下面以 服务端双向流 为例,展示 Callback API 的典型时序。
11.1 Mermaid 时序图
ServerBidiReactor gRPC Runtime Client ServerBidiReactor gRPC Runtime Client Reactor 创建 客户端发送第 1 条消息 处理 req_,生成 resp_ 客户端发送第 2 条消息 再次处理并生成响应 客户端关闭写方向,不再发送消息 没有更多输入,结束 RPC Chat(context) 返回 new ChatReactor() StartRead(&req_) msg1 OnReadDone(true) StartWrite(&resp_) resp1 OnWriteDone(true) StartRead(&req_) msg2 OnReadDone(true) StartWrite(&resp_) resp2 OnWriteDone(true) StartRead(&req_) WritesDone / half-close OnReadDone(false) Finish(Status::OK) RPC finished OnDone()
11.2 ASCII 版时序图
text
Client gRPC Runtime ServerBidiReactor
| | |
| |---- create reactor ---------->|
| |<--- StartRead(&req_) ---------|
| | |
|---- msg1 --------------->| |
| |---- OnReadDone(true) -------->|
| | |
| |<--- StartWrite(&resp_) -------|
|<--- resp1 ---------------| |
| |---- OnWriteDone(true) ------->|
| |<--- StartRead(&req_) ---------|
| | |
|---- msg2 --------------->| |
| |---- OnReadDone(true) -------->|
| |<--- StartWrite(&resp_) -------|
|<--- resp2 ---------------| |
| |---- OnWriteDone(true) ------->|
| |<--- StartRead(&req_) ---------|
| | |
|---- half-close --------->| |
| |---- OnReadDone(false) ------->|
| |<--- Finish(Status::OK) -------|
|<--- RPC end -------------| |
| |---- OnDone() ---------------->|
| | |
12. 把 Reactor 理解成状态机
理解 Callback API 的最好方式之一,就是把 Reactor 当作一个状态机。
例如在双向流场景中,它可能有这样的逻辑状态:
- 初始化状态:等待第一条输入
- 已收到消息:处理并准备响应
- 响应写出中
- 写完成:继续等待下一条输入
- 输入结束:准备结束 RPC
- 已完成:等待
OnDone()
状态流转示意
text
[创建 Reactor]
|
v
StartRead
|
v
OnReadDone(true)
|
+--> 处理请求
|
v
StartWrite
|
v
OnWriteDone(true)
|
v
StartRead
|
+--> 循环...
|
v
OnReadDone(false)
|
v
Finish(OK)
|
v
OnDone
所以 Callback API 并不是普通顺序程序,而是:
一个由事件不断驱动推进的状态机。
13. 与同步 API、CompletionQueue API 对比
13.1 与同步 API 对比
| 维度 | 同步 API | Callback API |
|---|---|---|
| 编程风格 | 顺序式 | 事件回调式 |
| 易理解性 | 高 | 中 |
| 流式表达能力 | 一般 | 强 |
| 高并发友好度 | 一般 | 更好 |
| 生命周期管理 | 简单 | 更复杂 |
同步 API 典型写法:
cpp
grpc::Status SayHello(
grpc::ServerContext* context,
const HelloRequest* request,
HelloReply* response) override {
response->set_message("Hello " + request->name());
return grpc::Status::OK;
}
Callback API 写法则变成:
cpp
grpc::ServerUnaryReactor* SayHello(
grpc::CallbackServerContext* context,
const HelloRequest* request,
HelloReply* response) override {
return new HelloReactor(request, response);
}
最大区别
- 同步 API:通过函数返回值结束 RPC
- Callback API :通过
Finish()结束 RPC
13.2 与 CompletionQueue API 对比
| 维度 | CompletionQueue API | Callback API |
|---|---|---|
| 控制粒度 | 非常高 | 高 |
| 代码复杂度 | 高 | 中 |
| 需要自己轮询事件 | 是 | 否 |
| 需要管理 tag | 是 | 否 |
| 状态机写法 | 显式 | 分散在回调中 |
| 可维护性 | 较难 | 更好 |
CompletionQueue 更底层,适合:
- 需要极细粒度控制
- 需要自定义复杂调度
- 团队已经熟悉 CQ 模式
Callback API 更适合:
- 想保留异步能力
- 又不想手写 CQ 事件循环
- 业务更偏事件驱动
14. 生命周期与资源管理
14.1 Reactor 对象的生命周期
一个 Reactor 的典型生命周期是:
- 被创建
- 发起若干异步操作
- 接收若干回调
- 调用
Finish() - 最终收到
OnDone() - 资源释放
14.2 为什么很多示例都写 delete this
因为服务方法通常会这样返回:
cpp
return new ChatReactor();
对象是在堆上分配的,而它的生命周期又跨越多个异步回调,因此经常在最终回调中释放:
cpp
void OnDone() override {
delete this;
}
14.3 为什么不要过早释放
如果你在异步操作尚未完成时就释放对象,gRPC 后续回调访问该对象会导致未定义行为。
因此:
Finish()不是销毁点OnDone()才是最终安全销毁点
15. 并发与线程安全注意事项
15.1 回调可能不是严格串行的
虽然从逻辑上你会把它看成一条状态机链路,但在实现上,不同方向的事件可能由不同执行上下文触发。
因此要假设:
- 回调之间可能存在并发
- 共享状态需要同步保护
15.2 共享状态要注意同步
如果多个回调里都会访问某些成员变量,就要考虑:
std::mutexstd::atomic- 更严格的状态机设计
15.3 读写方向是相对独立的
特别是在双向流中:
- 读完成事件
- 写完成事件
- 取消事件
它们在逻辑上并不总是严格按你想象的顺序串行。
因此不要写出依赖"某个回调一定最后发生"的脆弱逻辑,除了:
OnDone()是最终结束点
16. 常见坑与最佳实践
16.1 坑一:局部变量作为读写缓冲
错误示例:
cpp
void OnReadDone(bool ok) override {
ChatResp resp;
resp.set_text("hello");
StartWrite(&resp); // 错误:resp 很快就离开作用域
}
问题在于:
StartWrite()是异步的- 写完成前
resp必须一直有效 - 局部变量会提前析构
正确方式:
cpp
class ChatReactor : public grpc::ServerBidiReactor<ChatReq, ChatResp> {
private:
ChatResp resp_;
};
16.2 坑二:同时发起多个读
通常不应在前一个读尚未完成时再次调用 StartRead()。
错误倾向:
cpp
StartRead(&req1_);
StartRead(&req2_); // 不应这样做
通常应保持:
- 同一时刻最多一个 read 在进行中
16.3 坑三:同时发起多个写
同理,不应在前一个写尚未完成时再次调用 StartWrite()。
错误倾向:
cpp
StartWrite(&resp1_);
StartWrite(&resp2_); // 不应这样做
通常应保持:
- 同一时刻最多一个 write 在进行中
16.4 坑四:在回调里做重计算或阻塞 IO
回调函数不适合长时间阻塞,例如:
- 大量 CPU 计算
- 文件阻塞 IO
- 数据库长耗时同步调用
- 睡眠等待
否则会影响整体吞吐和响应性。
最佳实践:
- 重任务丢给业务线程池
- 回调里只做轻量状态推进
- 必要时通过异步任务与 Reactor 协同
16.5 坑五:误以为 Finish() 就代表已经完全结束
不是。
Finish():表示你请求结束 RPCOnDone():表示 RPC 已经彻底结束
最终资源清理应优先放在 OnDone() 中。
16.6 坑六:忽视取消场景
客户端可能随时取消 RPC,因此你需要考虑:
OnCancel()发生后是否还要继续处理- 后台任务是否要停止
- 状态是否要标记为 cancelled
16.7 最佳实践总结
- 把 Reactor 视为一个状态机
- 把消息缓冲对象放在成员变量中
- 保证一次只有一个读、一个写在飞
- 回调里尽量轻量
- 在
OnDone()做最终清理 - 共享状态要考虑线程安全
- 明确处理取消路径
17. 适用场景
Callback API 非常适合以下场景:
17.1 流式 RPC
比如:
- 实时聊天
- 日志推送
- 订阅通知
- 传感器数据上报
- 音视频流控制通道
17.2 高并发异步服务
希望:
- 避免同步阻塞式写法
- 同时又不想自己维护 CompletionQueue
17.3 事件驱动业务
例如:
- 收到一条消息,立即触发某个异步处理
- 写完成后继续下一轮交互
- 根据客户端输入推进会话状态
17.4 长生命周期 RPC
如:
- 长连接会话
- 双向流对话
- 逐步协商式协议
18. 不一定适合的场景
以下场景下,Callback API 不一定是最佳选择。
18.1 极其简单的 Unary RPC
如果逻辑非常简单,直接使用同步 API 可能更清晰。
18.2 团队不熟悉回调/状态机思维
Callback API 虽然比 CQ 简洁,但仍然有以下门槛:
- 生命周期复杂
- 回调链思维
- 并发状态管理
18.3 非常强调底层控制
如果你需要:
- 非常细粒度的异步调度控制
- 自定义事件循环和复杂交互
- 与其他底层框架深度整合
那么 CompletionQueue API 可能更适合。
19. 总结
gRPC C++ Callback API(Reactor 模式)的本质可以概括为:
把 RPC 处理写成一个事件驱动的状态机,由 gRPC 在读、写、取消、结束等时机回调你的 Reactor。
它的关键特点有:
- 不需要自己维护 CompletionQueue
- 通过 Reactor 对象承载异步逻辑
- 通过
StartRead/StartWrite发起异步操作 - 通过
OnReadDone/OnWriteDone推进流程 - 通过
Finish()请求结束 RPC - 通过
OnDone()做最终清理
一句话记忆版
同步 API
"我现在把这个 RPC 处理完,然后返回结果。"
CompletionQueue API
"我自己拉事件、分发事件、维护状态机。"
Callback / Reactor API
"事件到了你通知我,我在回调里推进下一步。"
最后建议
如果你正在学习 gRPC C++,推荐这样掌握 Callback API:
- 先理解 Unary Reactor
- 再掌握 Server Streaming
- 然后学习 Client Streaming
- 最后重点练习 Bidirectional Streaming
因为双向流最能体现 Reactor 模式的精髓:
- 回调驱动
- 状态推进
- 生命周期管理
- 并发与异步思维