grpc的二进制序列化与http的文本协议对比

发布于:2025-06-04 ⋅ 阅读:(42) ⋅ 点赞:(0)

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 与固定长度

  1. Varint 可变长度整数

    • Protobuf 用“可变长度整数(varint)”来编码整型、布尔等:值越小,占用字节就越少。
    • 例如 123 编码为 0x7B(1 字节)就能表示,若是更大的数,才会用 2–5 字节逐步展开。
    • 而 JSON 无论是多小的整数,也要用对应的 ASCII 字符“1”、“2”、“3”各 1 字节,外加引号或其他符号,不够紧凑。
  2. 定长类型避免额外开销

    • Protobuf 对于 floatdoublefixed32fixed64 等类型,直接用 4 或 8 字节二进制表示,不需要转成文本,也没有额外空白字符。
    • JSON 先要把浮点数转换成 ASCII(例如 3.14 是 4 字符),如果更长就更多字节。

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 请求包含两部分:

  1. 请求行 + 头部(Headers)
  2. 请求体(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)

  1. 二进制帧(Binary Framing & Multiplexing)

    • HTTP/2 将所有请求和响应拆分成固定格式的二进制帧,而不是像 HTTP/1.x 那样纯文本“逐行发送”。

    • 每个帧都有一个 9 字节的帧头(Frame Header),记录该帧的类型、流(Stream)ID、Payload 长度等,然后跟随 N 字节的实际负载(Payload)。

    • 比如一次 gRPC 调用,关键帧类型是:

      1. HEADERS 帧:承载 HTTP/2 层面的请求头(经 HPACK 编码)。
      2. DATA 帧:承载真正的 Protobuf 二进制数据(外加 gRPC 自身的 5 字节前缀)。
  2. HPACK 头部压缩(HPACK Header Compression)

    • HPACK 维护两张表:

      1. 静态表(Static Table):预定义常见头部名称/值(如 :method, :path, content-type, 等),发送时直接用索引替换具体字符串。
      2. 动态表(Dynamic 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 帧,其中包括:

  1. 伪头字段(Pseudo-Headers)

    • :method: POST
    • :scheme: https
    • :authority: api.example.com
    • :path: /UserService/GetUser
  2. 普通头字段

    • 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)初次请求
  1. 建立 TCP/TLS + HTTP/2 连接的一次性开销

    • 握手后,客户端和服务端在同一持久连接上可以反复交互,不会每次都重新协商。
  2. 发送 HEADERS 帧(假设压缩后约 64 字节)

    • an HTTP/2 帧头:9 字节
    • HPACK 压缩后的头部:约 55 字节
    • 合计:约 64 字节
  3. 发送 DATA 帧(15 字节 gRPC Payload + 9 字节帧头)

    • gRPC Payload(5 字节前缀 + 10 字节 Protobuf 二进制)= 15 字节
    • HTTP/2 DATA 帧头:9 字节
    • 合计:约 24 字节
  4. 合计

    • 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. 体积更省带来更好性能

  1. 更少的网络带宽

    • 报文更小,TCP 分段减少,拥塞控制更快稳定,丢包重传成本也更低。
  2. 更少的序列化/反序列化开销

    • Protobuf 的二进制解析直接把字节映射到内存结构,CPU 开销远低于 JSON 的文本解析(需要字符串拆分、数字转换、Unicode 解码等)。
  3. 更好的缓存友好性

    • 紧凑的二进制数据更容易装入 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 开销,从而整体性能大幅提升。

网站公告

今日签到

点亮在社区的每一天
去签到