背景
入职公司后发现,大量业务之间的调用都是通过 RPC 服务完成的。由于之前对 RPC 只停留在浅显的理解层面,开发中一旦遇到相关问题总会感到棘手。另一方面,gRPC 以其高效性能、完善生态以及大厂背书,已经成为大多数公司底层广泛采用的方案。因此决定系统性地学习 gRPC 的底层原理,并整理成这篇笔记,介绍其中一些重要知识点。至于具体的代码实现部分,内容最后推荐一位 B 站 up 主老师的课程,讲解通俗易懂,我的笔记也主要是基于他的课程整理而成~
1. 什么是 RPC / gRPC
随着公司业务不断扩展,系统通常会被拆分为多个微服务,不同的服务分布在不同的服务器上。此时一个核心问题出现了:服务与服务之间如何高效通信。如果直接依赖底层的网络调用,不仅复杂繁琐,还会严重影响开发效率。
为此,RPC(Remote Procedure Call,远程过程调用) 诞生。它的核心思想是:让调用者能够像调用本地方法一样调用远程服务的方法,从而屏蔽底层的通信细节,极大地提升了开发的便利性。
在众多 RPC 框架中,gRPC 脱颖而出:
- 由 Google 开源 的高性能 RPC 框架
- 源于 Google 内部的 Stubby RPC 系统,并于 2015 年正式开源
- 已成为 云原生时代的事实标准,被广泛应用于大规模分布式系统
2. 核心设计思路
- 网络通信:gRPC封装了网络通信部分,提供多种语言的网络通信封装(C、Java[Netty]、Go)
- 协议:基于HTTP2协议,传输二进制数据内容,支持双向流(双工)和连接的多路复用
- 序列化:使用protobuf(Protocol Buffers)序列化方式,时间效率和空间效率是JSON的3-5倍
- 代理创建:通过stub让调用者像调用本地方法那样调用远端服务方法
3. gRPC与ThriftRPC的区别
共性:都支持异构语言的RPC
区别:
- 网络通信:Thrift使用TCP专属协议,gRPC使用HTTP2
- 性能角度:ThriftRPC性能高于gRPC
- 生态支持:gRPC有大厂(Google)背书,在云原生时代与其他组件合作更顺利,应用更广泛
4. gRPC的优势
- 高效的进程间通信
- 支持多种语言(C、Go、Java等)和多平台(Linux、Android、iOS、macOS、Windows)
- 使用protobuf序列化方式,效率高
- 基于HTTP2协议
- 有大厂(Google)背书和支持
上面介绍完了gRPC是什么,相比之下优势有哪些,下面详细介绍一下我觉得很重要的亮点, protobuf序列化方式和http2.0协议,以帮助大家更好的理解为什么这两点优化可以大大提高gRPC的效率~
5. Json 与 Protobuf 对比
1. 编码格式:二进制 vs. 文本
这是最根本的区别。
- JSON (文本格式):
- 使用人类可读的字符串(Unicode)来表示所有数据。
- 数字
12345被存储为字符'1','2','3','4','5',在网络传输或内存中需要 5 个字节。 - 布尔值
true被存储为字符't','r','u','e',需要 4 个字节。 - 缺点:体积大,需要转换(解析/序列化)。
- Protobuf (二进制格式):
- 使用紧凑的二进制字节来表示数据。
- 数字
12345根据其值大小,可以使用 1-5 个字节(使用 Varint 编码,小数字占字节更少)。 - 布尔值
true仅用 1 个比特位 (bit) 就可以表示(通常是1)。 - 优点:体积极小,计算机无需解析即可理解大部分内容。
2. 数据模型:强 Schema vs 无 Schema
- JSON (无 Schema):
- 传输数据时,必须包含所有的字段名。
- 例如:
{"userId": 12345, "userName": "Alice"} - 字段名
"userId"和"userName"作为字符串每次都需要传输和存储,浪费了大量空间。
- Protobuf (强 Schema):
- 依赖预先定义好的
.proto文件(如message User { int32 id = 1; string name = 2; })。 - 传输数据时,不传输字段名,只传输字段对应的数字标签 (Field Number) 和值。
- 例如,上面的数据可能被编码为:
08 B9 60 12 05 41 6C 69 63 65(十六进制)。08:标签号 (1) 和数据类型 (Varint)。B9 60:数字 12345 的 Varint 编码。12:标签号 (2) 和数据类型 (字符串长度)。05:字符串长度为 5。41 6C 69 63 65:字符串 "Alice" 的 ASCII 字节。
- 优点:用简短的数字标签(1, 2, 3...)替代冗长的字段名,极大减少了数据体积。
- 依赖预先定义好的
3. 序列化/反序列化过程:计算密集型 vs 简单解码
- JSON:
- 序列化:将内存中的对象转换为字符串,涉及大量字符串拼接和转义。
- 反序列化:解析字符串,需要词法分析(tokenization)、语法分析(parsing),将字符串转换回数字、布尔值等。这是一个非常计算密集型的过程,尤其对于大型嵌套对象。
- Protobuf:
- 由于有预定义的 Schema 和二进制格式,编码和解码过程更像是一个简单的“翻译”。
- 编码:根据 Schema,将字段按标签号顺序,用预定义的二进制格式(如 Varint)写入字节流。
- 解码:读取字节流,根据标签号查找 Schema,就知道接下来要读的数据类型和含义,直接写入内存。
- 优点:CPU 开销极低,速度非常快。
4. 数据类型支持
- JSON: 类型系统简单(number, string, boolean, array, object, null),无法直接表达更复杂的类型如枚举、日期、字节数组等,需要约定俗成,增加了复杂性和不确定性。
- Protobuf: 内置支持多种高效的数据类型,如:
- Varint: 对小的整数编码体积非常小。
- 定长编码: 如
fixed32,double,对大的数字或浮点数效率更高。 - 直接支持
bytes(字节数组)、enum(枚举)等类型。
5. 对比
| 特性 | JSON | Protobuf | 结果 |
|---|---|---|---|
| 编码格式 | 文本(字符串) | 二进制(字节) | Protobuf 体积小 |
| 数据表示 | 包含字段名 | 只含数字标签 | Protobuf 体积小 |
| 序列化 | 拼接/转义字符串 | 直接写二进制 | Protobuf CPU开销低,速度快 |
| 反序列化 | 词法/语法分析(慢) | 直接解码(快) | Protobuf CPU开销低,速度快 |
| Schema | 无(需猜测) | 强约束(.proto文件) | Protobuf 无歧义,工具链强大 |
| 人类可读 | 是(优点) | 否(需工具解析) | JSON 易于调试 |
正是因为避免了传输冗余的字段名、使用紧凑的二进制编码以及省去了复杂的解析过程,Protobuf 在时间和空间效率上实现了对 JSON 的数量级超越。在微服务、gRPC、大数据存储等对性能和带宽敏感的场景中,Protobuf 是远比 JSON 更优秀的选择。而 JSON 则在需要人类可读性和简单性的场景(如 Web API)中继续发挥着重要作用。
6. HTTP 2.0 协议
HTTP/1.x 协议回顾
- HTTP/1.0 采用请求-响应模式,是一种短连接、无状态的协议。其传输数据为文本格式,通信方式为单工,无法实现服务器主动推送,通常通过客户端轮询等方式间接实现推送功能。
- HTTP/1.1 在1.0的基础上实现了有限长连接,并可通过升级机制使用 WebSocket 实现全双工通信,从而支持服务器向客户端推送消息。
HTTP/1.x 的共同特点:
- 采用文本格式传输数据,可读性较好,但效率较低;
- 虽然采用了websocket,但本质上仍无法实现真正的双工通信;
- 请求多个资源时需建立多个连接,效率低下。
HTTP/2 协议的核心特性
- 采用二进制格式传输数据,相比 HTTP/1.x 效率显著提升,但可读性较差;
- 支持真正的全双工通信;
- 实现了多路复用:仅通过一个连接即可并发请求多个资源,大幅提升性能。
HTTP/2 的三个基本概念
- 流(Stream):已建立的连接中的双向字节流;
- 消息(Message):对应于HTTP中的请求或响应,由一个或多个帧组成;
- 帧(Frame):HTTP/2 的最小通信单位,每个帧包含帧头及负载内容。
其他重要机制
- 优先级控制:可为不同的 Stream 设置权重,调整资源传输的先后顺序;
- 流量控制:支持客户端和服务器之间基于窗口机制进行流控,防止某一端过量发送数据导致对方处理不过来。
HTTP/2 协议不仅将传输格式转为二进制从而显著提升效率,更从根本上实现了真正的双工通信,支持服务端主动向客户端推送消息。其多路复用机制允许在单一连接中并行处理多个请求,极大提升了网络性能与用户体验。正因如此,HTTP/2 为 gRPC 框架提供了高效的底层通信能力。
7. gRPC 的四种通信方式
1. 一元 RPC(Unary RPC)
这是最基础的请求-响应模式,与传统的 HTTP REST API 调用类似,也是开发中绝大多数场景所使用的通信方式。
- 工作方式:客户端发送一个请求到服务器,服务器处理后再返回一个响应。
- 类比:就像普通的函数调用。
- 适用场景:简单的查询、提交数据等需要一次性请求和响应的操作。例如,通过用户ID获取用户信息。
2. 服务器端流式 RPC(Server Streaming RPC)
服务器可以返回一个消息流来响应客户端的单个请求。
- 工作方式:客户端发送一个请求,服务器可以返回一系列(多个)响应。这些响应会按顺序流式地发送回客户端,直到所有消息发送完毕。
- 类比:客户端像是一次性提出了一个问题,服务器则一边生成答案一边分批发送,就像“涓涓细流”。
- 适用场景:服务器需要传输大量数据或需要长时间处理并向客户端持续发送结果的场景。例如,服务器向客户端实时推送日志文件、发送大量的查询结果、或推送实时股票K线数据。
3. 客户端流式 RPC(Client Streaming RPC)
客户端可以发送一个消息流到服务器,服务器在接收完所有消息后返回一个响应。
- 工作方式:客户端持续地发送一系列消息给服务器。服务器等接收完所有消息后,再返回一个单一的响应。
- 类比:客户端像在“上传”或“提交”一个由多个分片组成的大文件,全部上传完毕后,服务器返回一个“上传成功”的确认。
- 适用场景:客户端需要上传大量数据或连续发送多个请求后等待一个汇总结果的场景。例如,上传一个大文件、批量上传传感器读数后等待处理结果。
4. 双向流式 RPC(Bidirectional Streaming RPC)
客户端和服务器可以同时、独立地向对方发送一个消息流。这两个流是相互独立的,允许全双工通信。
- 工作方式:双方都使用一个读写流来发送一系列消息。客户端和服务器可以按任意顺序读写消息——服务器可以在响应客户端消息之前等待接收所有消息,也可以边读边写,或者采用其他任何组合方式。
- 类比:就像一场实时双向的电话对话,双方都可以随时说话。
- 适用场景:需要实时、双向交互的场景。这是 gRPC 最强大的模式。例如,实时聊天应用、多人游戏中的状态同步、物联网设备间的指令与控制等。
对比
| 通信方式 | 客户端行为 | 服务器行为 | 典型场景 |
|---|---|---|---|
| 一元 RPC (Unary) | 发送 1 个请求 | 返回 1 个响应 | 普通 API 调用 (如 getUser) |
| 服务器端流式 | 发送 1 个请求 | 返回 N 个响应 | 推送日志、股票行情 |
| 客户端流式 | 发送 N 个请求 | 返回 1 个响应 | 文件上传、数据采集 |
| 双向流式 | 发送 N 个请求 | 返回 N 个响应 | 实时聊天、游戏同步 |
这四种模式共同构成了 gRPC 强大而灵活的通信基础,使其能够高效地适应从简单查询到复杂实时交互的各种应用场景。其高效性正是建立在之前讨论的 HTTP/2 协议的多路复用、二进制帧等特性之上的。
总结
在云原生不断发展的今天,服务之间的通信与调用大多依赖于 RPC。只有从底层更深入地理解其原理,才能在开发过程中思考得更全面,设计得更合理。关于 gRPC 的具体实现,如果大家有兴趣学习,在这里推荐一位 B 站 up 主老师的课程,讲解非常清晰、深入:
2145

被折叠的 条评论
为什么被折叠?



