想象你管理着一个餐饮集团,旗下几十家分店散落在不同城市,每家后厨说着不同的“方言”。过去,分店之间要互相查库存、调材料,只能派人骑着自行车跑过去翻对方的账本——你得知道对方用什么本子、什么格式。后来集团想了个办法:把所有菜品编进一本统一的电子菜单,每道菜都配上唯一编号;再配一辆装着隔板的冷藏车,一次出发就能同时配送几十个分店的订单。分店之间再也不用互相熟悉对方,只需照着菜单喊一声,餐盒就会在专车上飞驰而至。 这套“电子菜单 + 密封餐盒 + 冷藏车队”的组合,正是 gRPC 与 Protobuf 的缩影:gRPC 把“喊一声”变成一种跨店通用的规则,Protobuf 则把菜品信息封装进紧凑的编号餐盒,冷藏车在底层高效调度。两者一环扣一环,让现代软件世界里的服务之间沟通又快又稳。接下来,我们就跟着这个餐饮集团的故事,一步步拆开 gRPC 与 Protobuf 的秘密。

岗位关联

对 AI 从业者而言,掌握 gRPC 与 Protobuf,意味着模型推理、特征服务能在大规模服务系统中高效互联,多语言团队协作时少走一半弯路。

目录
  1. 可视化演示
  2. 初识gRPC
  3. Protobuf编码
  4. HTTP/2特性
  5. 调用模式
  6. 拦截器机制
  7. REST对比
  8. 工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

\u901a\u8fc7\u4ea4\u4e92\u5f0f\u6587\u6c0f\u56fe\u5c55\u793a\u96c6\u5408\u7684\u5e76\u96c6\u3001\u4ea4\u96c6\u3001\u5dee\u96c6\u8fd0\u7b97\uff0c\u52a8\u6001\u9ad8\u4eae\u8fd0\u7b97\u7ed3\u679c\u533a\u57df\u5e76\u663e\u793a\u5143\u7d20\u6570\u91cf\u3002

本节配有交互动效(进入视口后自动加载,约 3KB)。

\u96c6\u5408\u8fd0\u7b97\u6587\u6c0f\u56fe \u96c6\u5408A \u96c6\u5408B \u8fd0\u7b97\u7ed3\u679c A\u222aB = {x | x\u2208A \u6216 x\u2208B}

初识gRPC

假设你管理着一家餐饮集团,旗下几十家分店散落各地,每家后厨各说各话。过去,一家店想查另一家店的库存,得派人骑车过去翻对方的台账——你得知道对方用什么本子、什么格式、什么流程。集团痛定思痛,印发了一本《分店交流手册》:统一的问话模板、统一的数据表格、统一的骑手服务。任何分店要问什么,只需照着手册喊一声,骑手便跑腿代办。这本手册,就是 gRPC 的雏形。

软件开发里,“分店互相问话”叫服务间通信(RPC,即像调用本地函数一样调用远程服务)。gRPC 是 Google 开源的远程过程调用框架,它规定了一种“手册”——.proto 接口定义文件。开发者先规定接口和数据结构,再自动生成各种语言的客户端调用代码。于是,一个用 Java 写的订单服务,可以像调用本地函数一样,去调用一个用 Go 写的支付服务。

具体流程三句话:写一个 .proto 文件声明 service 和 message;用 protoc 编译,生成各语言 stub(客户端调用桩,相当于每个分店手里的“点菜键盘”);服务端实现接口,客户端调用 stub。gRPC 默认基于 HTTP/2 传输,天然获得多路复用与流式传输能力。因为 .proto 是语言无关的文本契约,语言壁垒被彻底打通。

所以,《分店交流手册》解决的是“问得明白、跑得统一”。有了 gRPC,即使分店用着完全不同的锅碗瓢盆(编程语言),只要都按手册办事,“喊一嗓子”就能拿到想要的答案。

result=Service.Method(request)\displaystyle \mathrm{result} = \mathrm{Service}.\mathrm{Method}( \mathrm{request} )
这个公式是 gRPC 调用的本质:把“跨网络调用”伪装成一次普通的方法调用。服务名、方法名、参数都藏在点号和括号里,框架帮你完成剩下的网络旅程。
  1. Service服务对象名,对应 .proto 里的 service 块——相当于报出分店名,让骑手知道去哪儿取货。
    Service∈{OrderService, PayService}\mathrm{Service} \in \{ \mathrm{OrderService},\ \mathrm{PayService} \}
  2. Method方法名,服务暴露的具体接口——相当于菜品名,告诉后厨要做什么菜。
    Method∈{CreateOrder, CancelOrder}\mathrm{Method} \in \{ \mathrm{CreateOrder},\ \mathrm{CancelOrder} \}
  3. request请求参数,按 message 定义被 Protobuf 序列化成二进制餐盒。
    request:message\mathrm{request} : \mathrm{message}
  4. result返回结果,同样以二进制消息形式回传并解析。
    result=response message\mathrm{result} = \text{response message}

Protobuf编码

当骑手来到分店取餐,他要的并不是一大段文字描述,而是一只贴着编号的密封餐盒。集团规定:1 号是宫保鸡丁,2 号是鱼香肉丝,3 号是酸辣汤……如果一份订单里同时要三样菜,后厨就封三个盒子,每个盒子只贴最小巧的编号。接收方不看菜名,只看编号,就能准确知道盒子里是什么。

这个密封餐盒就是 Protobuf 序列化后的消息。Protobuf(Protocol Buffers)是 Google 的二进制序列化协议,核心思想是:字段名不出现在消息里,取而代之的是“字段编号 + 编码类型”的 key(键),后面紧跟紧凑编码的值。这和 JSON 里把字段名原样写出来有本质区别。

在 .proto 中,每个字段必须分配唯一编号,例如 int32 user_id = 1;。序列化时,Protobuf 把编号乘以 8(即左移 3 位),与 3 比特的 wire type(线缆类型,表示值的编码方式)相加,生成一个 key。wire type 为 0 表示 Varint 变长整数,2 表示长度前缀。解码端靠 key 识别字段身份和取法,遇到不认识的字段编号就跳过,从而实现向前兼容。

后厨用编号而非菜名,换来的是更小的餐盒和更快的装车。解析方不用认识每道菜,只要会看编号就能正确分拣;遇到新菜(新字段)也从容跳过。这就是 Protobuf 在“省空间”和“能进化”之间找到的平衡。

key=(field_number×8)+wire_type\displaystyle \mathrm{key} = (\mathrm{field\_number} \times 8) + \mathrm{wire\_type}
这行公式是 Protobuf 编码的“门牌号生成器”。想象每个字段是酒店的一个房间,key 就是门牌:前半段是房间号(字段编号),后半段是锁的种类(wire type),两者拼在一起,服务员一看便知。
  1. field_number字段编号,在 .proto 里手工指定,唯一且必须保持稳定——相当于菜谱上的编号。
    field_number=1, 2, 3, …\mathrm{field\_number} = 1,\ 2,\ 3,\ \dots
  2. × 8等价于左移 3 位,为 wire type 腾出低 3 位空间。
    ×8 ⇔ 左移 3 位\times 8 \ \Leftrightarrow \ \text{左移 3 位}
  3. wire_type值的编码方式,0 表示 Varint,2 表示 length-delimited——相当于锁的种类。
    wire_type∈{0, 1, 2, 3, 4, 5}\mathrm{wire\_type} \in \{ 0,\ 1,\ 2,\ 3,\ 4,\ 5 \}
  4. 相加把编号与类型信息拼进同一个 key。
    key=8+0=8 (即 0x08)\mathrm{key} = 8 + 0 = 8 \ (\mathrm{即}\ 0x08)

HTTP/2特性

外卖升级之前,一辆车一次只送一单。如果东城店同时有 30 个订单要送,就得派 30 辆小车,路上堵成一锅粥。集团换来了大冷链车,车厢里用隔板分成几十个货位,每个订单的餐盒按货位码好,一路并行飞奔,到站再按货位卸货。

这辆大冷链车就是 HTTP/2。HTTP/2 是 gRPC 的默认传输层,它有三大法宝:二进制分帧、多路复用、头部压缩。这让高并发通信不再依赖“多开连接”,而是让一条连接承载几十路并发流量。

HTTP/2 把一切数据切成帧,每帧有一个 9 字节的头部,记录 stream id(流标识,相当于货位号)、类型、标志和长度。多个 stream 的帧在同一条 TCP 连接上交错发送,接收端按 stream id 重新组装,互不等待,彻底解决 HTTP/1.1 的队头阻塞问题。HPACK 头部压缩则把重复的头部字段映射成小整数,把开销量降到极低。

有了大冷链车,gRPC 在传输层实现了“一车多单、并行不排队”。这也是它与 REST 在性能上拉开差距的关键原因之一。

frame=⟨stream_id, type, flags, length⟩⏟frame header+payload\displaystyle \mathrm{frame} = \underbrace{\langle \mathrm{stream\_id},\ \mathrm{type},\ \mathrm{flags},\ \mathrm{length} \rangle}_{\text{frame header}} + \mathrm{payload}
一帧数据就像高速路上的一辆集装箱小车:车头贴着车牌(stream_id)、货物类型(type)、是否到站(flags),车厢里才是真正的货物(payload)。接收方靠车头信息分拣,即使所有车混在同一条路上也不怕。
  1. stream_id流标识,标注这帧属于哪条业务流——是 1 号货位还是 2 号货位。
    stream_id=1, 3, 5, …\mathrm{stream\_id} = 1,\ 3,\ 5,\ \dots
  2. type帧类型,如 HEADERS、DATA、RST_STREAM——是托盘还是冷鲜盒。
    type∈{HEADERS, DATA, SETTINGS}\mathrm{type} \in \{ \mathrm{HEADERS},\ \mathrm{DATA},\ \mathrm{SETTINGS} \}
  3. flags标志位,如 END_STREAM 表示流结束——相当于“最后一件行李已上车”。
    END_STREAM=1\mathrm{END\_STREAM} = 1
  4. payload帧承载的数据——餐盒本体。
    payload=serialized message\mathrm{payload} = \text{serialized message}

调用模式

点餐的姿势不止一种。有人按部就班,点一份等一份;有人点“厨艺直播”,后厨边做边传,一道菜一个镜头;有人抱着整筐食材交给后厨,最后要一张汇总验收单;还有人干脆拿起对讲机和后厨实时沟通。gRPC 把这四种姿势原样做进了框架。

这四种模式分别是:一元调用(Unary)、服务端流式(Server Streaming)、客户端流式(Client Streaming)、双向流式(Bidirectional Streaming)。它们的本质区别,在于请求与响应的“条数”组合不同。

一元调用是一个请求对应一个响应,和普通函数调用没有区别;服务端流式适合推送,一个查询换来持续不断的日志、行情、AI 生成 token;客户端流式适合批量上传,边发边等,最后收一个汇总;双向流式让双方随时互发,适合实时协作。HTTP/2 的 stream 为这四种模式提供了天然载体,每条流互不阻塞。

从“一问一答”到“对讲机聊天”,调用模式的选择完全取决于业务节奏。gRPC 把流式能力变成了随手可用的开关,让视频直播、批量上传这类交互体验成为可能。

调用模式={一元:m=1, n=1服务端流式:m=1, n=∞客户端流式:m=∞, n=1双向流式:m=∞, n=∞\displaystyle \text{调用模式} = \begin{cases} \text{一元:} & m = 1,\ n = 1 \\ \text{服务端流式:} & m = 1,\ n = \infty \\ \text{客户端流式:} & m = \infty,\ n = 1 \\ \text{双向流式:} & m = \infty,\ n = \infty \end{cases}
这里 m 代表请求条数,n 代表响应条数。四种模式就是 m 和 n 在“一”与“多”之间的四种组合,像四种点餐姿势一样各有各的适用场景。
  1. m=1, n=1一元调用——柜台点一份菜,后厨端一份上来。
    m=1, n=1m = 1,\ n = 1
  2. m=1, n=∞服务端流式——点一个“直播”,后厨边做边传,连续上菜。
    m=1, n=∞m = 1,\ n = \infty
  3. m=∞, n=1客户端流式——交一整筐食材,最后拿一张汇总验收单。
    m=∞, n=1m = \infty,\ n = 1
  4. m=∞, n=∞双向流式——对讲机自由通话,双方随时说话。
    m=∞, n=∞m = \infty,\ n = \infty

拦截器机制

每次送餐进厨房,都要过三道安检:保安看健康码,领班记客流,质检抽检餐盒。这些事跟炒菜本身没关系,但少了任何一道,门店都会出乱子。如果都让厨师自己做,厨房早就乱套了。gRPC 把这个思路做成了拦截器(Interceptor)。

拦截器允许开发者在请求到达业务方法之前(以及响应返回之后)挂载逻辑,用来统一处理鉴权、日志、限流、监控、重试等横切关注点,业务代码保持干净。它就像一道道安检闸门,请求先过闸,再进厨房。

gRPC 支持两类拦截器:客户端拦截器在请求发出前介入,注入 trace ID、统计耗时、做重试;服务端拦截器在请求进入 handler 前介入,校验 token、限流、打审计日志。多个拦截器可以嵌套成链,一层层筛过去,任何一层都能直接拦截,也可以给请求补充信息后放行。

安检门独立于厨房,想加装就加装、想撤换就撤换。拦截器让横切逻辑与业务解耦,把“与做菜无关但必须做”的事挡在业务代码之外,这是 gRPC 在生产环境可维护性高的原因。

resp=In(⋯I2(I1(req))⋯ )\displaystyle \mathrm{resp} = I_n( \cdots I_2( I_1( \mathrm{req} ) ) \cdots )
请求像客人进门一样穿过一道道安检门:先过 I1,再过 I2……最后到达业务方法;返回时再一层层穿回去。每一层都可以盖章、开包检查,甚至直接劝返。
  1. req进入拦截链的原始请求,包含方法、元数据与消息体。
    req=⟨method, metadata, msg⟩\mathrm{req} = \langle \text{method},\ \text{metadata},\ \text{msg} \rangle
  2. I1, I2, …, In各层拦截器,负责日志、鉴权、限流等横切逻辑。
    Ii:Context→ContextI_i : \text{Context} \to \text{Context}
  3. 嵌套调用请求一层层向内传递,最内层是真正的业务 handler。
    req→I1→I2→⋯→handler\mathrm{req} \to I_1 \to I_2 \to \cdots \to \mathrm{handler}
  4. resp响应带着各层补充的信息逐层返回给调用方。
    resp=⟨code, msg⟩\mathrm{resp} = \langle \text{code},\ \text{msg} \rangle

REST对比

明信片人人会写、到处能寄,但信息量小、投递慢;密封餐盒信息密度高、投递快,可拆盒需要专用工具。REST 和 gRPC 的区别,就像明信片与专车餐盒的区别:一个面向所有人开放,一个面向内部高效协作。

REST 基于 HTTP/1.1 + JSON,是人类可读的文本协议,curl 一敲就能调试,浏览器原生支持;gRPC 基于 HTTP/2 + Protobuf,是紧凑的二进制协议,强类型、自动生成代码,但调试需要 grpcurl 或专用客户端。两者在性能、可读性、适用场景和生态上各有取舍。

性能上,gRPC 的二进制编码和头部压缩使报文体积通常缩小 60% 以上,多路复用让并发不再排队;可读性上,REST 完胜,任何人用浏览器地址栏都能看明白请求长什么样;生态上,REST 对公网开放 API 更友好,gRPC 更适合内部服务间高频调用。浏览器场景下,gRPC 还需要 gRPC-Web 网关做协议转换。

明信片没有被淘汰:给客人发优惠券、贴公告,用明信片依然方便;但分店之间的高频调货和库存同步,用专车加餐盒更经济。选型的关键不是谁替代谁,而是场景在哪。

η=SJSON−SprotoSJSON×100%\displaystyle \eta = \frac{ S_{\mathrm{JSON}} - S_{\mathrm{proto}} }{ S_{\mathrm{JSON}} } \times 100\%
这是“信息瘦身率”:把同一个对象分别用 JSON 和 Protobuf 编码,看 Protobuf 替我们省下了多少体积。真实业务消息往往能省出一大半空间。
  1. S_JSONJSON 编码后的字节数——明信片的字数。
    SJSON=bytes(json)S_{\mathrm{JSON}} = \text{bytes}(\text{json})
  2. S_protoProtobuf 编码后的字节数——密封餐盒的体积。
    Sproto=bytes(proto)S_{\mathrm{proto}} = \text{bytes}(\text{proto})
  3. 差值除以 JSON 体积相对节省比例。
    SJSON−SprotoSJSON\frac{S_{\mathrm{JSON}} - S_{\mathrm{proto}}}{S_{\mathrm{JSON}}}
  4. × 100%换算成百分比,直观展示“瘦身”效果。
    ×100%\times 100\%

工程实践

把这一整套体系铺到所有分店,靠人肉翻译菜单不可行。总部搭建了工具链:一份电子菜单(.proto)输入进去,自动生成各分店能用的点菜键盘(stub)和后厨流程图(服务骨架),还配好了送达时限(deadline)、补送规则(retry)、全程 GPS(trace)。

真实工程里,gRPC 落地有四件事绕不开:代码生成、超时重试、流量控制、链路监控。它们让框架从“能跑”变成“好跑”。protoc 加各类插件,一份 .proto 同时产出 Java、Go、Python 的 stub 与服务骨架,契约永不走样。

deadline 机制给每次调用设定最迟完成时间,配合重试策略消化瞬时故障;HTTP/2 流控和负载均衡保证资源不被拖垮;metadata 里传递的 trace ID 把跨服务调用编成一条完整链路,供监控系统观测。这一整套实践,让 gRPC 成为大规模分布式系统的首选通信方式之一。

至此,集团的传菜体系成型:统一菜单(.proto)、密封餐盒(Protobuf)、冷链车队(HTTP/2)、四种点餐姿势(调用模式)、安检门(拦截器)、以及完整工具链。分店再多,也像一家店在协调运转。

stub=codegen(G)\displaystyle \mathrm{stub} = \mathrm{codegen}( \mathcal{G} )
从“菜单”到“键盘”:代码生成器读取 .proto 中定义的 service 集合 G,为不同语言生成对应的 stub。开发者唯一要维护的核心产物,就是那份 .proto 文件。
  1. G.proto 文件里的 service 集合——集团的统一菜谱。
    G={OrderService, PayService}\mathcal{G} = \{ \mathrm{OrderService},\ \mathrm{PayService} \}
  2. codegenprotoc 编译工具,把菜谱翻译成各分店的语言。
    codegen=protoc+language plugins\mathrm{codegen} = \text{protoc} + \text{language plugins}
  3. stub生成的客户端调用桩,调用方只需要调 stub 方法。
    stub=generated client classes\mathrm{stub} = \text{generated client classes}

AI 应用场景

实时特征服务的高并发调用

阶段:在线推理服务链路

解决的问题:推荐与广告系统在线推理时,需要同时拉取用户画像、历史行为、物品向量等多个特征源。传统 REST + JSON 报文臃肿、串行拉取延迟高;改用 gRPC + Protobuf 后,多个请求可以在一根 HTTP/2 连接上并行复用,报文更小,端到端延迟大幅下降。

具体案例:某推荐团队把特征服务切换为 gRPC,三个特征源并行拉取,报文体积平均减少 55%,P99 延迟从 120ms 降到 60ms,CPU 占用也随之降低。

大模型推理的流式对话输出

阶段:LLM 推理服务的流式响应

解决的问题:大模型逐字生成时,如果等全部生成完再返回,用户要等几十秒;用 HTTP 轮询又浪费资源。借助 gRPC 服务端流式模式,推理服务把生成的 token 持续推送给应用层,体验从“等一整页”变成“像流水一样读”。

具体案例:某 ChatBot 后端调用 LLM 推理服务时采用 gRPC 双向流式,客户端可随时发送新问题,服务端持续回传 token,前端还原出打字机效果,首字延迟从 2s 降到 0.5s。

Python 演示

python32 行
"""演示 Protobuf 核心编码原理:Varint 与字段 Tag."""def encode_varint(value):    """将整数编码为变长字节序列."""    out = bytearray()    while value > 127:        out.append((value & 0x7F) | 0x80)        value >>= 7    out.append(value)    return bytes(out) def decode_varint(data):    """从字节序列中解码出整数."""    result = 0    shift = 0    for b in data:        result |= (b & 0x7F) << shift        if not (b & 0x80):            break        shift += 7    return result def encode_field(field_no, wire_type, value):    """模拟 proto 字段:将标签与值都编码为 varint."""    tag = (field_no << 3) | wire_type    return encode_varint(tag) + encode_varint(value) if __name__ == "__main__":    for n in (1, 127, 128, 300, 16384):        raw = encode_varint(n)        print("%6d -> %s -> %d" % (n, list(raw), decode_varint(raw)))    sample = encode_field(1, 0, 150)    print("字段1 Varint(150) 完整编码:", list(sample))
目的:用纯 Python 实现 Protobuf 的 Varint 编码与字段 Tag 拼接,直观理解“字段编号如何变成线上字节”。
  1. out.append((value & 0x7F) | 0x80)取低 7 位,并打上“后面还有字节”的继续位标记。
  2. value >>= 7右移 7 位,准备编码下一段。
  3. result |= (b & 0x7F) << shift解码时把每字节的低 7 位放回正确的位移位置。
  4. tag = (field_no << 3) | wire_type这正是“Tag 机制”的核心:编号与线缆类型拼成 key。
  5. sample = encode_field(1, 0, 150)模拟 .proto 中 int32 num = 1 字段写入 150 时的真实编码。
  6. print(...)输出每个整数的原始值、编码字节、解码还原,验证一致。

常见误区

误区“Protobuf 就是 JSON 的压缩版吧?去掉空格、转成二进制、体积小一点,仅此而已。”

正解完全不是。Protobuf 在编码时会丢掉字段名,只传字段编号与 wire type;它有一套 schema(.proto)约束结构,支持 Varint 紧凑编码,还天然具备向前兼容能力。就像密封餐盒不是把明信片折起来,而是根本不写字,只贴编号。没有 schema 和编号机制,就没有 Protobuf 的一切。

为何常见:人们看到“二进制体积更小”的表象,忽略了背后的核心是“字段名不传输 + 编号映射 + 编码规则”这套完整设计。

误区“gRPC 是 Google 出的,只能在 Go 里用吧?我是写 Python 的,学它没用。”

正解gRPC 官方支持 C++、Java、Python、Go、Ruby、C# 等十多种语言。.proto 是语言中立契约,能生成各语言 stub,跨语言互调正是它的核心卖点。Python 服务调用 Go 服务、Java 服务调用 C++ 服务,都是日常操作。

为何常见:gRPC 在 Go 生态里出镜率太高,加上大量教程用 Go 演示,容易让其他语言开发者误以为与自己无关。

误区“字段编号就是个序号,我按心情排,反正功能看起来都正常。”

正解字段编号是线上传输的 key 的一部分。一旦发布,新旧版本都会读取这个编号;随意改动等于换菜编号,旧餐盒全部读错。正确的做法是:新增字段只加新编号,已删除的编号用 reserved 保留,永不复用。

为何常见:开发早期只有自己一个服务在用,看不出影响;等到跨团队协作、服务升级时,才意识到编号是兼容性的地基。

误区“gRPC 性能吊打 REST,现在还选 REST 的人肯定不懂技术。”

正解REST 在浏览器原生支持、调试友好性、公开 API 生态上有不可替代的价值。gRPC 虽然内部性能更优,但面向公网和浏览器时,需要 gRPC-Web 网关转换,反而增加复杂度。两者是“明信片与专车”的关系,各归其位。

为何常见:只看性能对比指标,忽略了可读性、浏览器支持、开放生态等同样重要的维度。

面试题

Q1. gRPC 默认使用哪种底层传输协议?

单选题 · easy

  • A.HTTP/1.1
  • B.HTTP/2← 正确答案
  • C.裸 TCP 长连接
  • D.WebSocket

答案:B。gRPC 默认建立在 HTTP/2 之上,看中的正是它的多路复用、二进制分帧和头部压缩。就像集团的大冷链车,专为多订单并行配送而生。HTTP/1.1 是旧小车,一次只能送一单;裸 TCP 和 WebSocket 缺少 gRPC 需要的帧规范与流式语义。

Q2. 在 .proto 文件中,字段编号的主要作用是什么?

单选题 · easy

  • A.标记字段在源码中的书写顺序
  • B.唯一标识字段,用于序列化与反序列化← 正确答案
  • C.决定字段是整数还是字符串
  • D.指定 gRPC 的负载均衡策略

答案:B。字段编号是 Protobuf 的“菜谱编号”。编码时它被写进 key,解码时靠它定位字段,字段名反而不出现在消息里。所以编号必须唯一且稳定,一旦随意改动,新旧服务解析就会错乱。

Q3. 以下哪个场景最适合用 REST 而不是 gRPC?

单选题 · easy

  • A.两个内部微服务之间的高频调用
  • B.服务端持续推送行情数据
  • C.面向外部开发者提供公开 API,且需浏览器直接调用← 正确答案
  • D.需要跨多种编程语言自动生成客户端代码

答案:C。公开 API 强调可读性和浏览器兼容,REST 用 curl 即测、浏览器原生支持,像明信片一样人人可读。内部高频调用、流式推送、强类型代码生成则分别对应 gRPC 的优势。

Q4. Protobuf 中 wire_type = 0 表示值采用什么编码?

单选题 · mid

  • A.固定 64 位(fixed64)
  • B.Varint 变长整数← 正确答案
  • C.长度前缀的字节串
  • D.固定 32 位(fixed32)

答案:B。wire_type 0 对应 Varint,即用尽量少的字节编码小整数,像餐盒把“一张纸”折到最小。固定 64 位是 wire_type 1,固定 32 位是 wire_type 5,长度前缀是 wire_type 2。

Q5. HTTP/2 的多路复用主要解决了什么痛点?

单选题 · mid

  • A.数据包丢失无法恢复
  • B.同一条连接上的请求需排队等待、前一个响应阻塞后一个← 正确答案
  • C.HTTP 头部无法自定义
  • D.无法支持 HTTPS

答案:B。HTTP/1.1 时代,同一连接同时只能处理一个请求,后面的请求得排队,称为队头阻塞。HTTP/2 用 stream 让多个请求的帧交错并行,就像冷链车隔出几十个货位,不同订单同时运输,不再“一辆车只送一单”。

Q6. 客户端连续上传一批日志,希望服务端最终统一返回一个处理结果,应选用哪种 gRPC 调用模式?

单选题 · mid

  • A.一元调用
  • B.服务端流式
  • C.客户端流式← 正确答案
  • D.双向流式

答案:C。客户端流式的特征是“请求多条、响应一条”,像抱着整筐食材交给后厨,最后拿到一张汇总验收单。一元是“一问答一答”,服务端流式是“一问回多条”,双向流式是“边聊边答”。

Q7. 用 Varint 编码整数 300 需要几个字节?

单选题 · extreme

  • A.1
  • B.2← 正确答案
  • C.3
  • D.4

答案:B。300 的二进制是 1 0010 1100(9 位),Varint 按 7 位一组拆:低位组 0101100,高位组 0000010,再给低组补“还有后续”标记位,得到 [0xAC, 0x02],共 2 字节。注意不能是 1 字节(超过 7 位),也不需要 4 字节——这正是“餐盒被压缩”的体现。

Q8. 关于 gRPC 拦截器的说法,以下哪项是正确的?

单选题 · mid

  • A.拦截器只能用于客户端
  • B.拦截器能统一处理鉴权、日志等横切逻辑← 正确答案
  • C.拦截器会修改 .proto 定义
  • D.拦截器的出现是为了替代元数据

答案:B。gRPC 同时有客户端拦截器和服务端拦截器,常用于鉴权、日志、限流、监控等横切逻辑,相当于安检门:请求在进入业务厨房前先过几道闸,业务代码保持干净。它不会改 .proto,也不会替代元数据。

Q9. gRPC 支持以下哪些调用模式?(多选)

多选题 · mid

  • A.客户端流式← 正确答案
  • B.服务端流式← 正确答案
  • C.双向流式← 正确答案
  • D.单一流式

答案:ABC。gRPC 的四种调用模式是:一元、服务端流式、客户端流式、双向流式。D 选项“单一流式”并不存在,属于干扰项。四种模式的本质是请求与响应的条数组合不同。

Q10. 关于 Protobuf 的向前兼容性,以下做法正确的有哪些?(多选)

多选题 · hard

  • A.删除字段后立刻复用该字段编号给新字段
  • B.新版本只新增字段编号,不修改旧编号← 正确答案
  • C.随意改变已有字段的编号
  • D.保留已废弃字段的编号并标记 reserved← 正确答案

答案:BD。向前兼容的关键在于字段编号。新增字段只加新编号,老客户端遇到不认识编号会跳过,故 B 对。已删除的编号应通过 reserved 保留不用,否则新老版本可能解析错乱,故 A 错、D 对。修改已有编号等于换菜编号,会让存量数据全部读错,故 C 错。