首页
/ gRPC 核心概念详解:从 .proto 接口定义到 HTTP/2 之上的帧协议与流控

gRPC 核心概念详解:从 .proto 接口定义到 HTTP/2 之上的帧协议与流控

2026-09-05 11:10:25作者:宣利权Counsellor

本文基于 gRPC 官方仓库中的核心概念文档 CONCEPTS.md 展开,系统讲解 gRPC 的接口抽象(IDL 与代码生成)、同步/异步调用模型、流式(Streaming)语义,以及 gRPC 抽象协议如何在 HTTP/2 上落地为带 5 字节帧头的长度前缀帧。读完后,你不仅能理解一次 gRPC 调用在客户端、服务端之间究竟发生了什么,还能结合仓库中的示例 proto 与服务端/客户端实现,看懂协议中每个组成部分(Call Header、Initial-Metadata、Payload Messages、Status、Trailing-Metadata)对应的实际行为。

一、接口:以语言无关的服务描述为起点

gRPC 的本质是对远程过程调用(RPC)的具体实现:客户端像调用本地函数一样调用远端服务,而这一抽象的起点是一份语言无关的 RPC 服务描述(一组方法的集合)。gRPC 的 Protocol Compiler 插件会基于这份描述生成各受支持语言中的客户端与服务端接口——客户端通过这些生成的 API 发起远程调用,服务端则实现对应的接口来响应调用。

默认情况下,gRPC 使用 Protocol Buffers 作为接口定义语言(IDL),同时描述服务接口和载荷消息的结构;如有需要也可以使用其他替代方案(例如 JSON,详见下文协议部分 content-typeapplication/grpc+json 的约定)。

仓库中的示例 proto 是理解这一机制的最佳素材。以 helloworld.proto 为例,一份典型的服务定义长这样:

syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}

  rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {}

  rpc SayHelloBidiStream (stream HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

这份 .proto 文件同时定义了:

  • 服务 Greeter 及其三个方法(一元、服务端流、双向流,正好覆盖下文的调用模型与流式语义);
  • 请求消息 HelloRequest 与响应消息 HelloReply 的字段结构与字段编号。

代码生成的实现位于 src/compiler 目录,其中 cpp_plugin.ccpython_plugin.ccnode_plugin.ccruby_plugin.cccsharp_plugin.ccobjective_c_plugin.cc 等各自对应一种受支持语言的 protoc 插件,src/proto/grpc/ 下则存放了 gRPC 官方自带的 proto 定义。

二、发起与处理远程调用:同步与异步两种编程面

RPC 希望尽可能贴近"过程调用"的抽象,因此同步调用(阻塞直至服务端返回响应)是最接近这一抽象的形态;但网络本质上是异步的,许多场景下希望在当前线程不被阻塞的前提下发起调用。为此,gRPC 在大多数语言中的编程面都提供同步与异步两种风格

这一点在 examples/cpp/helloworld 目录的文件组织上体现得非常直接:

文件 调用风格
greeter_client.cc / greeter_server.cc 同步客户端 / 同步服务端
greeter_async_client.ccgreeter_async_client2.cc / greeter_async_server.cc 基于 Completion Queue 的异步客户端 / 服务端
greeter_callback_client.cc / greeter_callback_server.cc 回调风格的客户端 / 服务端

也就是说,同一个 Greeter 服务在 C++ 侧就有同步、CQ 异步、回调三套可直接运行的实现范式,读者可以按自己的并发模型自由选择。

三、流式(Streaming):单个 RPC 上的多消息语义

gRPC 支持流式语义:在单个 RPC 调用中,客户端或服务端(或双方)都可以发送一条消息流。最一般的情形是双向流(Bidirectional Streaming)——一次 gRPC 调用建立起一条流,双方各自向其发送消息流。流式消息按发送顺序投递(ordered delivery)。

结合上文 proto 示例,gRPC 的四种调用形态一目了然:

形态 请求 响应 示例方法
一元 单条消息 单条消息 SayHello
服务端流 单条消息 消息流 SayHelloStreamReply
客户端流 消息流 单条消息 可在任意 service 中以 rpc M (stream Req) returns (R) 声明
双向流 消息流 消息流 SayHelloBidiStream

仓库中还有专门的流式示例 hellostreamingworld.proto,其中 MultiGreeter.sayHello 方法根据请求中的 num_greetings 字段回复多条问候,对应 examples/python/hellostreamingworldexamples/node 等目录下的可运行客户端/服务端,可作为服务端流的最小实操参考。

从协议视角看,"流式"并非特殊机制:无论一元还是流式,一次调用都是一条双向消息流(见下节),一元调用只是"流中恰好只有一条 Payload Message"的特例,因此流式与一元在传输层完全同构。

四、gRPC 抽象协议:一次调用的消息原子

CONCEPTS.md 中定义的抽象协议描述了一次 gRPC 调用由哪些部分构成:

  • 客户端 → 服务端方向:以强制的 Call Header 开始,随后是可选的 Initial-Metadata,再是零条或多条 Payload Messages。客户端通过底层协议机制(在 HTTP/2 上即 END_STREAM 标志)来宣告自己消息流的结束。
  • 服务端 → 客户端方向:包含可选的 Initial-Metadata,随后是零条或多条 Payload Messages,最后以强制的 Status 和可选的 Status-Metadata(又称 Trailing-Metadata)收尾。

注意两个关键约束:

  1. Call HeaderStatus强制的——前者承载方法名、路径等调用定义信息,后者承载 gRPC 状态码,即使成功也必须发送;
  2. 客户端消息流的结束由底层传输表达,而不是在 gRPC 层写一个特殊的"结束消息"。

五、HTTP/2 上的具体实现:帧结构、编码与状态传递

上述抽象协议的具体落地是基于 HTTP/2 的,完整细节见 doc/PROTOCOL-HTTP2.md。核心映射关系如下:

5.1 抽象概念与 HTTP/2 机制的对应

gRPC 抽象概念 HTTP/2 上的承载
gRPC 双向流 HTTP/2 stream(一条 gRPC 调用 = 一条 HTTP/2 流)
Call Header + Initial-Metadata HTTP/2 请求头(HEADERS + CONTINUATION 帧),受 HPACK 压缩
Payload Messages DATA 帧,内部为长度前缀的 gRPC 帧,发送方切分为 HTTP/2 DATA 帧、接收方重组
Status + Trailing-Metadata HTTP/2 尾随头(trailers)
客户端消息流结束 最后一个 DATA 帧上设置 END_STREAM 标志

5.2 请求头中的调用定义

doc/PROTOCOL-HTTP2.md 的 ABNF 规则,请求头即调用定义:

Request-Headers → Call-Definition *Custom-Metadata
Call-Definition → Method Scheme Path [Authority] TE [Timeout] Content-Type
                  [Message-Type] [Message-Encoding] [Message-Accept-Encoding] [User-Agent]

其中几个要点值得展开:

  • Path 形如 /Service-Name/Method,大小写敏感。文档明确指出,若 Path 不采用该形式,一些功能(例如 service config 支持)将无法工作;
  • Timeout 通过 grpc-timeout 头传递,取值为最多 8 位的十进制整数加单位字符(H/M/S/m/u/n 对应时/分/秒/毫秒/微秒/纳秒);若省略 Timeout,服务端应视为无限超时
  • Content-Type 必须以 application/grpc 开头(可带 +proto+json 等后缀);若不满足,服务端应当以 HTTP 415(Unsupported Media Type)应答,以此防止其他 HTTP/2 客户端把 gRPC 错误响应(HTTP 状态码恒为 200)误判为成功;
  • 压缩协商通过 grpc-encoding(消息编码)与 grpc-accept-encoding 完成,编码可以是 identitygzipdeflatesnappy 或自定义。

5.3 消息帧:1 字节标志 + 4 字节长度 + 消息体

DATA 帧中承载的是 Length-Prefixed-Message 序列,其线格式为:

Length-Prefixed-Message → Compressed-Flag Message-Length Message
Compressed-Flag  → 0 / 1        ; 1 字节无符号整数
Message-Length   → {Message 长度} ; 4 字节无符号整数(大端)
Message          → *{binary octet}

即每条 gRPC 消息在流上都是 5 字节头 + 消息体 的形式。规则上有两条容易踩坑的细节:

  • Compressed-Flag 为 1 表示消息体按 grpc-encoding 头声明的机制压缩过;为 0 表示未编码。压缩上下文不跨消息边界保持——每条消息都必须新建压缩上下文;若未发送 grpc-encoding 头,该标志必须为 0;
  • 请求的 EOS(end-of-stream)由最后一个 DATA 帧上的 END_STREAM 标志表达;若流需要关闭但没有剩余数据要发,实现方必须发送一个带该标志的空 DATA 帧

5.4 响应与状态:Trailers 与 Trailers-Only

响应形式为:

Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only
Trailers → Status [Status-Message] [Status-Details] *Custom-Metadata
Status   → "grpc-status" 1*digit

要点:

  • grpc-status 是 0-9 的十进制数字(即 gRPC 状态码,0 表示 OK),通过 grpc-message 头携带百分号编码的状态文本,grpc-status-details-bin 可携带 base64 编码的附加错误详情;
  • 即使状态码是 OK,Status 也必须在 Trailers 中发送——这正是一元调用"结果放在哪"的协议层答案:响应体里不放状态,状态永远走 trailers;
  • Trailers-Only(无响应头与消息体、直接发 trailers)仅用于立即出错的调用场景,例如认证失败、快速失败的参数错误等;
  • 响应的流结束由携带 Trailers 的最后一个 HEADERS 帧上的 END_STREAM 标志表达。

5.5 自定义 Metadata 的编码约束

自定义 metadata(Custom-Metadata)分两类:ASCII 头与二进制头。二进制头的键名以 -bin 结尾,值必须按 RFC 4648 做 Base64 编码(HTTP/2 不允许头值中携带任意字节序列),实现方必须同时接受带填充与不带填充的 Base64 值,且应当输出不带填充的值。另外,以 grpc- 开头但未被协议占用的键名保留给 gRPC 未来使用,应用不应将其用作自定义 metadata。

六、流控:直接复用 HTTP/2 的机制

CONCEPTS.md 对流控的表述非常简短但信息量足够:gRPC 使用 HTTP/2 的流控机制,从而能够对"在途消息的缓冲内存"做细粒度控制。

这意味着:

  • 流控单元就是 HTTP/2 的 DATA 字节流,接收方通过 WINDOW_UPDATE 帧按流和按连接两个维度授予发送配额,发送方不得超额发送;
  • 由于 gRPC 帧(5 字节头 + 消息体)在 HTTP/2 层被切分成多个 DATA 帧,窗口耗尽会天然反压到 gRPC 的消息发送,无需 gRPC 层再维护一套独立的缓冲配额;
  • 对双向流而言,两个方向各自有独立的窗口,客户端的发送速度不会被服务端慢消费拖垮全局——这是"细粒度内存控制"的实际含义:缓冲内存的规模由窗口大小决定,而不是由消息数量决定。

这一设计与第五章的帧结构天然契合:接收方在重组 Length-Prefixed-Message 之前,HTTP/2 层已经保证了未重组数据的总量不超过窗口额度。

七、在仓库中继续深入

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384