gRPC C++ Callback API(Reactor 模式)介绍

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 的基本思路不是写"从上到下"的阻塞式逻辑,而是写成:

  1. 创建一个 Reactor
  2. 发起第一个异步动作
  3. 等待 gRPC 回调
  4. 在回调里决定下一步
  5. 直到最终结束 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::ClientUnaryReactor
  • grpc::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 执行流程

  1. gRPC 收到请求
  2. 调用服务方法,创建 Reactor
  3. Reactor 填充响应
  4. 调用 Finish(Status::OK)
  5. 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 的典型生命周期是:

  1. 被创建
  2. 发起若干异步操作
  3. 接收若干回调
  4. 调用 Finish()
  5. 最终收到 OnDone()
  6. 资源释放

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::mutex
  • std::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():表示你请求结束 RPC
  • OnDone():表示 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。

它的关键特点有:

  1. 不需要自己维护 CompletionQueue
  2. 通过 Reactor 对象承载异步逻辑
  3. 通过 StartRead / StartWrite 发起异步操作
  4. 通过 OnReadDone / OnWriteDone 推进流程
  5. 通过 Finish() 请求结束 RPC
  6. 通过 OnDone() 做最终清理

一句话记忆版

同步 API

"我现在把这个 RPC 处理完,然后返回结果。"

CompletionQueue API

"我自己拉事件、分发事件、维护状态机。"

Callback / Reactor API

"事件到了你通知我,我在回调里推进下一步。"


最后建议

如果你正在学习 gRPC C++,推荐这样掌握 Callback API:

  1. 先理解 Unary Reactor
  2. 再掌握 Server Streaming
  3. 然后学习 Client Streaming
  4. 最后重点练习 Bidirectional Streaming

因为双向流最能体现 Reactor 模式的精髓:

  • 回调驱动
  • 状态推进
  • 生命周期管理
  • 并发与异步思维

相关推荐
Byte不洛1 小时前
深入理解C++智能指针:从RAII到shared_ptr
c++·智能指针·raii·unique_ptr·shared_ptr·auto_ptr
云深麋鹿1 小时前
C++ | map&set的使用
开发语言·c++
Eiceblue1 小时前
锁定单元格 :C# 控制 Excel 单元格编辑权限
开发语言·c#·excel
lilong(DLC)1 小时前
Qt信号槽在异步连接时需要将参数进行复制吗?
开发语言·qt
沐知全栈开发1 小时前
RSS 参考手册
开发语言
贫民窟的勇敢爷们1 小时前
构建基于Python与机器学习的智能客服
开发语言·python·机器学习
shehuiyuelaiyuehao1 小时前
算法20,x的平方根
开发语言·python·算法
csbysj20201 小时前
.switchClass() 方法详解
开发语言
菜_小_白1 小时前
高性能线程池
linux·c++·设计模式