grpc的二进制序列化与http的文本协议对比
gRPC 默认使用 Protocol Buffers(Protobuf)做序列化,相比常见的 HTTP+JSON 等“文本协议”,在字节长度上往往要小很多,主要原因可以归纳为以下几点:
以下是在之前回答的基础上,将“较少的元信息开销”部分(第 4 节)的详细展开内容整合进来的完整说明。
1. 二进制格式 vs 文本格式
文本协议(如 JSON、XML)
字段名和值都以可读字符形式出现,包含了大量标点和空白。例如,一个简单的 JSON 对象:
{ "id": 123, "name": "Alice", "active": true }
它会在网络上按照 UTF-8 字节传输,每个字段名前后都需要双引号、冒号、逗号、空格、换行等,实际发送的字节串很容易达到数十字节。
Protocol Buffers(二进制)
Protobuf 会把每个字段打包成“字段编号 + 类型标记 + 值的二进制表示”,不再把字段名以文本方式保留,也不需要标点或空白来分隔。
比如,上面那段 JSON 序列化后在二进制里可能只占 7 字节左右(示例):
08 7B // Field 1 (varint):123 12 05 41 6C 69 63 65 // Field 2 (length‐delimited):"Alice" 18 01 // Field 3 (varint):true
这串二进制只有 7 字节。如果在 gRPC 中再加上 5 字节的 frame 前缀,整个请求也才 12 字节左右,而同样数据的 JSON 文本就可能在 40–50 字节。
2. 编码机制:Varint 与固定长度
Varint 可变长度整数
- Protobuf 用“可变长度整数(varint)”来编码整型、布尔等:值越小,占用字节就越少。
- 例如
123
编码为0x7B
(1 字节)就能表示,若是更大的数,才会用 2–5 字节逐步展开。 - 而 JSON 无论是多小的整数,也要用对应的 ASCII 字符“1”、“2”、“3”各 1 字节,外加引号或其他符号,不够紧凑。
定长类型避免额外开销
- Protobuf 对于
float
、double
、fixed32
、fixed64
等类型,直接用 4 或 8 字节二进制表示,不需要转成文本,也没有额外空白字符。 - JSON 先要把浮点数转换成 ASCII(例如
3.14
是 4 字符),如果更长就更多字节。
- Protobuf 对于
3. 没有字段名与标点
字段名只出现在 .proto 定义里,一旦编译生成代码后,就变成字段编号(tag)
.proto
文件里:message User { int32 id = 1; string name = 2; bool active = 3; }
“id”、“name”、“active”这些名字在最终的二进制里根本不存在,只保留数字编号
1、2、3
及类型信息,大大节省了每条消息中都重复带字段名的开销。
文本格式(JSON/XML)必须保留字段名和标点
- JSON 的每个 key 都要写一次带双引号的字段名,一个三四十个字段的对象,字段名重复出现,光名字就可能占几百字节。
4. 较少的元信息开销
在 HTTP 通信中,除了真正的“业务数据”(即 Body)所占的字节之外,“元信息”(Meta Information)也会产生额外开销。下面具体展开:
4.1 HTTP/1.1 请求的元信息组成与开销
一次典型的 HTTP/1.1 + JSON 请求包含两部分:
- 请求行 + 头部(Headers)
- 请求体(Body)
其中头部部分承载了大量元信息(路径、Host、Content-Type、Content-Length、User-Agent、Accept-Encoding 等),在网络上往往会占用几十到上百字节。下面以一个简单示例拆解各行所占字节数:
POST /UserService/GetUser HTTP/1.1\r\n
Host: api.example.com\r\n
Content-Type: application/json\r\n
Content-Length: 42\r\n
Accept-Encoding: gzip, deflate\r\n
User-Agent: curl/7.79.1\r\n
\r\n
{"id":123,"name":"Alice","active":true}
4.1.1 各部分字节数示例
请求行
POST /UserService/GetUser HTTP/1.1\r\n
- “POST ”:4 字节
- “/UserService/GetUser ”:20 字节
- “HTTP/1.1”:8 字节
- “\r\n”:2 字节
- 合计:约 34 字节
Host 头
Host: api.example.com\r\n
- “Host: ”:6 字节
- “api.example.com”:15 字节
- “\r\n”:2 字节
- 合计:约 23 字节
Content-Type 头
Content-Type: application/json\r\n
- “Content-Type: ”:14 字节
- “application/json”:16 字节
- “\r\n”:2 字节
- 合计:约 32 字节
Content-Length 头
Content-Length: 42\r\n
- “Content-Length: ”:16 字节
- “42”:2 字节
- “\r\n”:2 字节
- 合计:约 20 字节
Accept-Encoding 头
Accept-Encoding: gzip, deflate\r\n
- “Accept-Encoding: ”:17 字节
- “gzip, deflate”:13 字节
- “\r\n”:2 字节
- 合计:约 32 字节
User-Agent 头
User-Agent: curl/7.79.1\r\n
- “User-Agent: ”:12 字节
- “curl/7.79.1”:11 字节
- “\r\n”:2 字节
- 合计:约 25 字节
空行分隔
\r\n
- 2 字节
上述“请求行 + 所有头部 + 空行分隔”就已经约 34 + 23 + 32 + 20 + 32 + 25 + 2 = 168 字节。
请求体(Body)
{"id":123,"name":"Alice","active":true}
- 以 UTF-8 计数,共约 39 字节。
合计:168 字节(头部) + 39 字节(Body) = 207 字节
(还未算 TCP/IP、TLS 等网络层和传输层带来的额外开销。)
结论:一个非常简单的 HTTP/1.1 + JSON 调用,单纯“元信息(Header)”就可能达到 150–200 字节,Body 也因文本格式而明显冗余。
4.2 HTTP/2 帧结构与 HPACK 头部压缩
相比 HTTP/1.x,HTTP/2 做了两方面核心优化:二进制帧(Binary Framing)和HPACK 头部压缩(Header Compression)。
二进制帧(Binary Framing & Multiplexing)
HTTP/2 将所有请求和响应拆分成固定格式的二进制帧,而不是像 HTTP/1.x 那样纯文本“逐行发送”。
每个帧都有一个 9 字节的帧头(Frame Header),记录该帧的类型、流(Stream)ID、Payload 长度等,然后跟随 N 字节的实际负载(Payload)。
比如一次 gRPC 调用,关键帧类型是:
- HEADERS 帧:承载 HTTP/2 层面的请求头(经 HPACK 编码)。
- DATA 帧:承载真正的 Protobuf 二进制数据(外加 gRPC 自身的 5 字节前缀)。
HPACK 头部压缩(HPACK Header Compression)
HPACK 维护两张表:
- 静态表(Static Table):预定义常见头部名称/值(如
:method
,:path
,content-type
, 等),发送时直接用索引替换具体字符串。 - 动态表(Dynamic Table):会话期间按顺序缓存最近使用过的头部字段,重复发送时只需发索引或差分更新。
- 静态表(Static Table):预定义常见头部名称/值(如
过程示例:
- 首次发送某个字段:若在静态表能找到索引,就直接用索引编码;否则要按“长度前缀 + 实际字节”发送,该字节序列再经过 Huffman 编码进一步压缩。
- 后续发送相同字段:大多情况下只需发一个“动态表索引”,字节数骤降。
4.2.1 HEADERS 开销对比示例
HTTP/1.1 文本格式(示例)
POST /UserService/GetUser HTTP/1.1\r\n
Host: api.example.com\r\n
Content-Type: application/json\r\n
User-Agent: grpc-go/1.50.0\r\n
Accept-Encoding: gzip\r\n
Te: trailers\r\n
\r\n
(binary-data…)
- 逐行文本拼接,头部就轻易超过 150 字节(前面已详细拆分)。
HTTP/2 + HPACK(二进制格式)
首次建立 gRPC 连接并发起调用时,客户端会发送一个 HEADERS 帧,其中包括:
伪头字段(Pseudo-Headers):
:method: POST
:scheme: https
:authority: api.example.com
:path: /UserService/GetUser
普通头字段:
content-type: application/grpc
te: trailers
user-agent: grpc-go/1.50.0
grpc-accept-encoding: identity,gzip
grpc-encoding: identity
所有这些字段都会先用 HPACK 通过静态表索引或动态表索引进行压缩,再用 Huffman 或纯字节表示长度和字符串。假设压缩后实际 HEADERS 帧的负载部分只剩 55 字节,再加上 9 字节的 HTTP/2 帧头,整帧就是 64 字节。
后续调用重用同一连接时:
- 大部分头字段都已存在动态表,只需发送“动态表索引”或“差分更新”,HEADERS 帧的大小可能进一步缩减到 30–40 字节(含 9 字节帧头)。
4.3 gRPC 自身的 5 字节消息前缀
在 gRPC 协议中,每条消息(message)都会有一个固定的 5 字节前缀,格式如下:
| 1 字节 FLAG | 4 字节 MESSAGE_LENGTH | MESSAGE_DATA(二进制 Protobuf) |
- 第 1 字节 FLAG:通常是
0x00
,表示该消息未被压缩;若启用 per-message 压缩,则会标记不同压缩算法。 - 后 4 字节 MESSAGE_LENGTH:网络字节序(big-endian),表示后续 Protobuf 二进制数据的长度。
- MESSAGE_DATA:即时序列化后的 Protobuf 二进制数据。
例如,若 Protobuf 序列化结果是 10 字节,整条 gRPC Payload 便是:
0x00 // 1 字节 FLAG
0x00 0x00 0x00 0x0A // 4 字节长度(10)
[10 字节 Protobuf 二进制]
- 合计:5 + 10 = 15 字节。
- 这段 15 字节会被放进一个或者多个 HTTP/2 的
DATA
帧里,每个DATA
帧前面还要 9 字节帧头(Frame Header),如果分片则每个分片都各自占用帧头开销。
相比 HTTP/1.1,后者要额外发:
- 一个空行(2 字节 “\r\n”)
- “Content-Length: 42\r\n” 约 20 字节
- “Content-Type: application/json\r\n” 约 32 字节
- “Host: …” 等其他头部几十字节
可见 gRPC 的 5 字节前缀方式更紧凑,也避免了 HTTP/1.1 中必须“明文写 Content-Length”带来的冗余。
4.4 HTTP/1.1 vs HTTP/2 (gRPC)头部开销对比示例
下面用一个简化示例对比“同样的用户查询请求”,在 HTTP/1.1 + JSON 与 gRPC(HTTP/2 + Protobuf)下的字节开销:
4.4.1 HTTP/1.1 + JSON
POST /UserService/GetUser HTTP/1.1\r\n ← 34 字节
Host: api.example.com\r\n ← 23 字节
Content-Type: application/json\r\n ← 32 字节
Content-Length: 42\r\n ← 20 字节
Accept-Encoding: gzip, deflate\r\n ← 32 字节
User-Agent: grpc-go/1.50.0\r\n ← 25 字节
\r\n ← 2 字节
{"id":123,"name":"Alice","active":true} ← 39 字节
- 总计:约 34 + 23 + 32 + 20 + 32 + 25 + 2 + 39 = 207 字节(不含网络层与传输层开销)。
4.4.2 gRPC(HTTP/2 + Protobuf)初次请求
建立 TCP/TLS + HTTP/2 连接的一次性开销
- 握手后,客户端和服务端在同一持久连接上可以反复交互,不会每次都重新协商。
发送 HEADERS 帧(假设压缩后约 64 字节)
- an HTTP/2 帧头:9 字节
- HPACK 压缩后的头部:约 55 字节
- 合计:约 64 字节
发送 DATA 帧(15 字节 gRPC Payload + 9 字节帧头)
- gRPC Payload(5 字节前缀 + 10 字节 Protobuf 二进制)= 15 字节
- HTTP/2 DATA 帧头:9 字节
- 合计:约 24 字节
合计:
- HEADERS 帧:约 64 字节
- DATA 帧:约 24 字节
- 总计:约 88 字节
相较于 HTTP/1.1 的 207 字节,gRPC 同样的调用大约只需 88 字节,约节省了 57%。
后续调用重用连接:
- 大多数 HEADERS 字段都已进入 HPACK 动态表,只需发送少量索引或差分,HEADERS 帧可能只剩 30–40 字节(含帧头)。
- DATA 帧依旧约 24 字节。
- 整次调用可能只需 54–64 字节。
5. 示例对比
5.1 JSON 文本大小
{"id":123,"name":"Alice","active":true}
- UTF-8 编码后约 39 字节。
5.2 Protobuf 二进制大小
如果按 .proto
定义:
message User {
int32 id = 1;
string name = 2;
bool active = 3;
}
将相同数据序列化后,得到二进制(十六进制展示):
08 7B // Field 1 (varint):123
12 05 41 6C 69 63 65 // Field 2 (length‐delimited):"Alice"
18 01 // Field 3 (varint):true
- 这段二进制总共 10 字节(包含字段编号与类型标记)。
- 在 gRPC 中,加上 5 字节前缀 → 共 15 字节,再加 9 字节 HTTP/2 帧头 → 24 字节。
- 与 39 字节的 JSON Body 对比,Protobuf 本身就小 1/4~1/3,再加上帧头相对也更紧凑。
6. 体积更省带来更好性能
更少的网络带宽
- 报文更小,TCP 分段减少,拥塞控制更快稳定,丢包重传成本也更低。
更少的序列化/反序列化开销
- Protobuf 的二进制解析直接把字节映射到内存结构,CPU 开销远低于 JSON 的文本解析(需要字符串拆分、数字转换、Unicode 解码等)。
更好的缓存友好性
- 紧凑的二进制数据更容易装入 CPU 缓存,减少内存带宽占用;再加 HTTP/2 的 HPACK 头部压缩,重复头部几乎无需重新传送,整体带来更高吞吐、更低延迟。
7. 小结
- gRPC 使用 Protobuf 做二进制序列化,本质上省去了字段名、标点、空格等冗余字符;而 HTTP/JSON 需要将 key、value、标点符号、空格全当文本发送。
- Protobuf 用 varint、定长二进制等高效编码方式,大大节省整数、布尔等类型的长度;JSON 始终用 ASCII 文本表示数字和布尔。
- HTTP/2 通过二进制帧和 HPACK 头部压缩,进一步削减了头部元信息的重复传输;gRPC 自身的 5 字节消息前缀也比 HTTP/1.1 的 Content-Length、空行分隔更紧凑。
- 结果是,同样一份结构化数据,gRPC(HTTP/2 + Protobuf)发送的字节数常常只有 HTTP/JSON 的三分之一、四分之一甚至更低,这不仅节省了带宽,也降低了序列化/解析的 CPU 开销,从而整体性能大幅提升。