前言
本文根据https://github.com/grpc/grpc/tree/master/examples/cpp/interceptors进行C++ grpc 拦截器示例学习运行。更多的是学习记录,水平不高,能力有限,错漏之处,还请见谅。欢迎友好讨论。
环境信息
-
操作系统版本:ubuntu24.04
-
CMake版本:4.2.0
-
Git版本:2.43.0
-
GCC版本:gcc 13.3.0
-
OpenSSL版本: 3.0.13
在之前的教程https://mp.weixin.qq.com/s/50Tep3mq7wkuCzVtAF-2DA 中给出的是在Centos 7.6下的部署流程,现在我重装了操作系统为ubuntu 24.04,并且重新编译了grpc文件。相关二进制文件可以关注公众号 只做人间不老仙 ,后台发送 "grpc ubuntu 编译文件"获取我编译内容的压缩包 。
代码运行流程
编译
参考 https://mp.weixin.qq.com/s/50Tep3mq7wkuCzVtAF-2DA 克隆仓库 https://github.com/EarthlyImmortal/blog_code 并配置grpc依赖。
配置好后,可以先修改一下 start_build.sh 中的gcc和g++的路径。

在blog_code/interceptors目录下执行:
bash
./start_build.sh
完成编译。

运行
在blog_code/interceptors/build/server目录下运行服务器:
bash
./server

另起一个终端,在blog_code/interceptors/build/client目录下运行客户端:
bash
./client

服务器最终输出:

代码大部分是copy的grpc官方的示例,这里对cmake文件和目录结构做了调整,对部分注释或者日志做了调整。
代码分析
拦截器功能介绍
这个示例主要是测试C++ grpc的拦截器(interceptors)功能。拦截器用于实现不局限于特定单个 RPC 方法的通用逻辑。典型应用场景包括:
-
元数据处理
-
日志记录
-
故障注入
-
缓存管理
-
指标收集
-
策略强制执行
-
服务器端身份认证
-
服务器端权限授权
拦截器可以分为客户端拦截器和服务器拦截器,后面的例子中会分别介绍两种拦截器类型的使用。
值得注意的是,C++ grpc 的拦截器实现是实验性质的,仍处于 experimental(实验性)命名空间下,没有被纳入稳定的公共 API。这意味着在生产环境中无法使用。
实现分析
代码使用的是一个简单的键值存储示例,使用的是异步回调API,关于异步回调接口更详细的介绍可以参考https://mp.weixin.qq.com/s/4hU0XMHne7brzOexqw2bow。可以发现这个示例没有使用SSL认证。关于grpc 认证相关内容可以参考https://mp.weixin.qq.com/s/_54ixo8DrUU1rcIKMnGDkw。
下面分别来看客户端和服务器的实现。
客户端实现
首先来看客户端创建channel的流程,使用的接口是grpc::experimental::CreateCustomChannelWithInterceptors 而不是常用的grpc::CreateChannel(大多数示例都是这个接口,如认证示例,参考https://mp.weixin.qq.com/s/_54ixo8DrUU1rcIKMnGDkw)或者是`grpc::CreateCustomChannel`(如压缩示例,参考https://mp.weixin.qq.com/s/gKOPz3s4SaZAuCjt4Lku1Q)。命名空间中的`experimental`表示这个接口还是实验性质的,后面拦截器相关接口都在这个命名空间中。在创建channel的时候,传入了拦截器创建工厂对象列表,在本例中,只有`CachingInterceptorFactory` 一个拦截器创建工厂对象。

由于拦截器是为单次调用而设计的,所以需要注册拦截器创建工厂对象,每个拦截器类实例随着调用开始而创建,在调用结束后由框架销毁。这一点我们可以用一个简单的程序来验证一下。由于原来示例的客户端不太好使用同一个channel连续发送相同RPC请求,这里仿照https://mp.weixin.qq.com/s/4hU0XMHne7brzOexqw2bow中的客户端修改实现,代码在blog_code/interceptors/my_client中,会连续进行两次GetValues RPC调用。

同时在客户端拦截器构造和析构时加入日志:


编译运行,查看客户端输出:

可以发现拦截器创建和销毁两次,说明每次RPC调用都会创建一个拦截器。这两次调用的缓存也是两份。因为只是示例,所以只用了最简单的实现。如果是真实的缓存,肯定要一个全局的,不过那样又要考虑多线程访问的问题。
客户端拦截器相关类图如下:

下面我们来看一下具体的客户端拦截器类CachingInterceptor的实现,代码在blog_code/interceptors/common/caching_interceptor.h中,只有1个接口Intercept,分为不同的HOOK点处理,各个HOOK点的作用如下:
| HOOK点 | 触发时机 | 拦截器行为 |
|---|---|---|
PRE_SEND_INITIAL_METADATA |
客户端即将发送初始元数据(RPC 开始) | 设置 hijack = true,创建真正的后端 stub 和流 (stream_),并调用 methods->Hijack() 接管整个 RPC。 |
PRE_SEND_MESSAGE |
客户端即将发送一个请求消息(StartWrite) |
从请求中提取 key,检查 cached_map_,如果命中,将缓存的 value 存入成员变量 response_,不向真正后端发送任何数据;如果未命中,则通过 stream_->Write(req) 向后端发送请求,同步调用 stream_->Read(&resp) 等待响应,将响应的 value 存入 response_ 和 cached_map_。 |
PRE_SEND_CLOSE |
客户端调用 WritesDone(不再发送消息) |
调用 stream_->WritesDone() 通知真正后端写结束。 |
PRE_RECV_MESSAGE |
客户端即将接收一个响应消息 | 将预先保存在 response_ 中的 value 写入到 GetRecvMessage() 返回的响应对象中。对于缓存命中的请求,这里返回的是本地缓存值;对于未命中的请求,返回的是从后端拉取的值。 |
PRE_RECV_STATUS |
客户端即将接收最终状态 | 将状态强制设置为 Status::OK,掩盖后端可能返回的任何错误(简化示例)。 |
注意在CachingInterceptor::Intercept接口最后调用的 Hijack()和 Proceed()。Hijack() 表示完全接管RPC,只在 PRE_SEND_INITIAL_METADATA 调用一次。调用后,该拦截器完全接管 RPC 的生命周期,请求不再发往真正的传输层。如果注册了多个拦截器,在拦截器链中排在 hijacking 拦截器之后的拦截器将不会被执行,但排在它之前的拦截器在正向和反向阶段仍然会正常执行;而Proceed() 表示当前拦截器已完成对本批次的处理,将控制权沿拦截器链传递------在正向阶段(SEND 相关)传给下一个拦截器直至传输层,在反向阶段(RECV 相关)传给上一个拦截器直至调用方。每次 Intercept() 被调用时,必须且只能调用 Hijack() 或 Proceed() 其中之一,否则 RPC 将永久阻塞。本身C++ 拦截器是实验性质的,这里也就不再深究验证这些规则了。

示例中为了化简,有些地方并不完备,比如在拦截器中访问server时,使用的是同步接口;并未处理后台返回的异常。毕竟是一个示例,重点还是在展示拦截器的基础使用方案。
官方的客户端的时序图如下:

服务器实现
首先来看创建服务器的流程,这里与常规流程的区别是增加了一个对SetInterceptorCreators的调用,用于设置服务器拦截器创建工厂对象列表。

与客户端拦截器类似,这里也是传入的拦截器创建工厂对象, 每个拦截器类实例随着调用开始而创建,在调用结束后由框架销毁。同样的,这一点我们可以用my_client和server验证一下。
首先在LoggingInterceptor的构造函数和析构函数中加入日志。

然后编译,运行。查看服务器输出。可以看到,因为my_client发送了两次RPC,所以服务器的拦截器创建和销毁2次。

服务器拦截器相关类图如下:

下面我们来看一下具体的服务器拦截器类LoggingInterceptor的实现,代码在blog_code/interceptors/server/server.cpp中,这个实现很简单,只有一个HOOK点,作用如下:
| HOOK点 | 触发时机 | 拦截器行为 |
|---|---|---|
POST_RECV_INITIAL_METADATA |
接收到客户端的初始 metadata(headers)之后,反序列化请求消息之前 | 打印 "Got a new streaming RPC" 日志,然后调用 Proceed() 放行。 |
只是多打了一条日志,没有做额外的行为。
其他问题
关于当前拦截器的HOOK点有哪些,可以看grpc/include/grpcpp/support/interceptor.h 的 enum class InterceptionHookPoints ,这里给出了当前的一些HOOK点枚举以及相关信息。

还有一个问题是,示例中注册的拦截器是对所有RPC调用生效的,但是目前只适配了GetValues这一个协议,如果有其他协议怎么办呢?可以有不同的解决思路。比如可以在拦截器创建工厂对象中判断当前协议类型来生成不同的拦截器对象;也可以在拦截器对象的Intercept 接口中根据不同的协议做不同的处理。但是实际上我感觉可能这个场景用拦截器不如直接在接口中实现缓存逻辑,因为并不通用,拦截器还是用于处理通用的逻辑更合适一些。
关于多个拦截器的处理顺序:正向是按照注册顺序,反向是按照注册顺序的逆序。
由于当前grpc C++ 的拦截器接口是实验性质的,所以不对这些内容再做过多的探究了。
参考资料
-
deepseek官方网站(https://www.deepseek.com/)辅助
-
https://github.com/grpc/grpc/tree/master/examples/cpp/interceptors
-
文中mermaid流程图源码路径: blog_code/interceptors/doc/mermaid.md
如果觉得文章还不错的话,欢迎点赞、关注、评论、转发,如果你有关于grpc 拦截器或者替代功能的实际使用经验,欢迎分享。我将持续更新C++后台相关知识。感谢感谢~