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

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

运行
在blog_code/metadata/build/server目录下运行服务器:
shell
./server

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

服务器最终输出:

代码大部分是copy的grpc官方的示例,这里对cmake文件和目录结构做了调整,对部分注释或者日志做了调整。
代码分析
元数据(Metadata)功能介绍
这个示例主要是测试grpc的元数据(Metadata)功能。元数据(Metadata)是一种"旁路通道",允许客户端与服务器之间相互传递与特定 RPC 调用相关联的信息。
grpc 元数据是以键值对(Key-Value Pair)形式存在的数据,随同 grpc 请求或响应的初始部分及最终部分一同发送。
grpc 元数据是通过 HTTP/2 头部(Headers)机制来实现的。其中的"键"(Keys)必须是 ASCII 字符串,而"值"(Values)既可以是 ASCII 字符串,也可以是二进制数据。键名不区分大小写,且禁止以 grpc- 作为前缀------该前缀已预留供 grpc 内部使用。
grpc 元数据可由客户端和服务器双方进行发送与接收。在一次 rpc 调用中,头部字段(Headers)会在客户端发送初始请求之前发往服务器,也会在服务器发送初始响应之前发往客户端;而尾部字段(Trailers)则由服务器在结束该次 rpc调用时发送。
grpc元数据常见应用场景包括:身份验证、分布式追踪、自定义头部(如应用于负载均衡、流量限速、提供错误信息等)。此外本身grpc内部也会使用元数据进行一些信息传输。
客户端和服务器实现分析
代码使用hellworld示例,且为同步接口,与mp.weixin.qq.com/s/50Tep3mq7... 中相同。可以发现这个示例没有使用SSL认证。关于grpc 认证相关内容可以参考mp.weixin.qq.com/s/_54ixo8Dr... 。
在示例中,客户端在发送请求协议的时候发送了两个头部字段(Headers)元数据,custom-header 和 custom-bin,服务器收到之后,对所有的客户端元数据字段进行了打印。其中custom-header以字符串形式输出,而custom-bin以-bin 结尾,被视为二进制数据,所以是按照十六进制数字输出的。


我们可以观察一下示例运行后的服务器输出,可以发现,除了客户端设置的两个头部字段(Headers)元数据,还有一个user-agent。这个是grpc框架添加的元数据,可以看到这里面包含了grpc版本、客户端操作系统等信息。

服务器发送响应时,发送了一个头部字段(Headers)元数据custom-server-metadata;发送了一个尾部字段(Trailers)元数据custom-trailing-metadata。客户端收到响应后,将这两个元数据以字符串形式输出。


客户端并没有将所有服务器响应的元数据都打印出来,我们可以对代码稍作修改,让客户端把服务器响应的元数据都打印出来,看看会不会有其他的元数据。修改如下:

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

可以看到没有其他的元数据了。
下面我们来进行一下抓包分析。看看元数据在请求和响应时是如何传输的。抓包分析流程可以参考 mp.weixin.qq.com/s/gKOPz3s4S... 的 客户端和服务器实现分析一节。
编译运行抓包分析,筛选grpc相关tcp包,可以发现还是只有两个,客户端到服务器一个;服务器到客户端一个。

首先分析第一个,也就是客户端到服务器的包。可以发现,wireshark在这个包里面解析出来了4个http2帧,这是因为grpc本身是基于http2传输的。分别是:HEADERS帧,WINDOW_UPDATE帧,DATA帧和另一个WINDOW_UPDATE帧。

WINDOW_UPDATE帧应该主要是用于做流量控制的,是http2的功能,我们不去深究。重点关注HEADERS帧和DATA帧。
首先看HEADERS帧的内容,可以看到一些基础请求头字段、grpc内部请求头(以grpc-开头)、元数据请求头都在这里。

DATA帧其实就是grpc消息数据,里面保存着是否压缩、数据长度以及编码后的具体pb消息。

我们再来看看服务器到客户端的响应。这里也有4个http2帧,其中WINDOW_UPDATE帧和DATA帧与客户端请求包类似,重点来看两个HEADERS帧。

首先是第一个HEADERS帧,包括一些基础请求头字段、grpc内部请求头(以grpc-开头)、服务器头部字段(Headers)元数据。

第二个HEADERS帧更简单一些,只包括grpc-status(grpc业务状态码)和服务器尾部字段(Trailers)元数据。

通过上面的分析,可以看出来,不管是客户端还是服务器,都符合头部字段(Headers)元数据先于请求/响应数据发送;而尾部字段(Trailers)元数据后于响应数据发送。
元数据大小上限问题
grpc消息中所有请求头的大小(包括基础请求头、grpc内部请求头和自定义元数据)是有限制的,这也限制了元数据的总大小。限制是通过定义在blog_code/grpc/include/grpc/impl/channel_arg_names.h中的GRPC_ARG_MAX_METADATA_SIZE和GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE两个参数决定的。
查看注释可以得到,GRPC_ARG_MAX_METADATA_SIZE是软上限,取8KB和0.8*GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE数值中的最大值。超过这个上限,会有部分请求会被随机拒绝。

而GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE是硬上限,取16KB和1.25*GRPC_ARG_MAX_METADATA_SIZE中的较大值。超过这个上限,全部请求都会被拒绝。

我们可以先测试一下客户端的请求头大小达到软上限但是没有到达硬上限的表现。测试代码放在blog_code/metadata/my_client和blog_code/metadata/my_server中,编译运行流程与 代码运行流程 一节中相同。只需要替换路径即可。
在客户端代码中添加如下增加请求元数据的代码,此时默认情况,软上限为8KB,硬上限为16KB,所以添加90个值长度为100字节的元数据。键长度+值长度应该在10KB左右。

编译运行,客户端输出如下,可以发现没有异常。根据注释,这个时候应该是部分请求被随机拒绝,所以这个表现正常。

我们把元数据提升到200个,超过硬上限,重新测试,客户端输出如下,RPC失败。根据注释,这个时候所有请求都会被拒绝,所以表现正常。

对这个流程抓包分析,如果过滤条件设置为grpc,可以看到,只有一条从客户端到服务器的消息,这条消息中有指定的元数据,但是没有从服务器到客户端的消息。

去除过滤条件,把所有的消息展开,可以看到在客户端到服务器消息之后,有一条服务器到客户端的http2消息,这是服务器的响应消息,状态码是8,与输出对应,查看 blog_code/grpc/include/grpcpp/support/status_code_enum.h可知,这个错误码代表某种资源用尽了。


这个抓包结果表明了,超过限制的判断是服务器做的,也就是消息接收方进行,不是请求方主动进行的。这也是符合逻辑的。
这两个上限 GRPC_ARG_MAX_METADATA_SIZE 和 GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE 是可以设置的,比如我们可以在服务器添加代码,设置 GRPC_ARG_MAX_METADATA_SIZE 为 30KB,代码如下:

则GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE为max(16KB, 1.25*30KB) = 37.5KB。还是添加200个值长度为100字节的元数据,编译运行,客户端输出如下 , 可以发现,没有拦截,因为既没有达到软上限,又没有达到硬上限。

而服务器将所有收到的元数据都打印了出来:

如果我们把元数据的数量改成500,重新测试,客户端输出如下。可以发现,因为超上限拦截了,错误信息中显示硬上限为 38400 = 37.5 * 1024,符合预期。

同样的,我们可以反向来测试客户端的拦截。在服务器端添加生成大量元数据的代码。

先按照200个自定义元数据测试,编译运行,客户端输出如下,可以发现,默认硬上限为16KB,RPC失败了。

对这个过程进行抓包分析。可以发现有请求包有回包,且回包状态码正确。说明这个超出判断是客户端自己做的。

同样的,客户端也可以调整上限,在客户端中添加如下代码,调整软上限为30KB,如前所述,硬上限则为37.5KB,注意,新代码在创建Channel时调用的是CreateCustomChannel而不是CreateChannel。

保持200个元数据,同时按照之前提到的调整client代码可以打印出所有的服务器元数据,编译运行,客户端输出如下,可以发现,运行正常。

将元数据数量调整到500,编译运行,客户端输出如下,rpc被拦截,显示硬上限为38400,符合预期。

相较于客户端向服务器发送元数据,服务器向客户端发送元数据可以分为头部字段(Headers)元数据和尾部字段(Trailers)元数据。那么当两者均存在时,是加起来不能超过上限还是单个不能超过上限即可?可以修代码进行测试,如下,添加200个头部字段(Headers)元数据和200个尾部字段(Trailers)元数据。

编译运行,查看客户端输出,可以发现协议正常,说明二者应该是单个不能超过上限即可。这一点也很容易理解,在前面的抓包分析中可以看到,头部字段(Headers)元数据和尾部字段(Trailers)元数据其实是分两个http2帧进行发送的。

在实际使用中,不建议将元数据大小上限调整的过大。过大的元数据可能会导致系统资源不足,上限太大,可能会容易遭受攻击。
总结下来就是:
- grpc元数据大小是由
blog_code/grpc/include/grpc/impl/channel_arg_names.h中的GRPC_ARG_MAX_METADATA_SIZE和GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE两个参数决定的。 GRPC_ARG_MAX_METADATA_SIZE是软上限,取8KB和0.8*GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE数值中的最大值。超过这个上限,会有部分请求会被随机拒绝。GRPC_ARG_ABSOLUTE_MAX_METADATA_SIZE是硬上限,取16KB和1.25*GRPC_ARG_MAX_METADATA_SIZE中的较大值。超过这个上限,全部请求都会被拒绝。- grpc元数据是否超过限制是由接收方判断的。
- grpc元数据上限可以调整,在实际使用中,不建议将元数据大小上限调整的过大。
参考资料
- 腾讯元宝-deepseek(yuanbao.tencent.com/ )和deepseek官方网站(www.deepseek.com/ )辅助
- github.com/grpc/grpc/t...
- grpc.io/docs/guides...
欢迎关注公众号:只做人间不老仙
如果觉得文章还不错的话,欢迎点赞、关注、评论、转发,如果你有关于grpc 元数据的实际使用经验,欢迎分享。我将持续更新C++后台相关知识。感谢感谢~