Protocol Buffers 技术解析:为什么叫「协议缓冲区」

Protocol Buffers 技术解析:为什么叫「协议缓冲区」

摘要

Protocol Buffers 是GRpc的序列化层,其核心设计理念源于「协议定义+二进制容器」的高效数据交换模式。

通过预定义数据结构作为协议约定,序列化时将字段名替换为数字编号,结合二进制编码、Varint压缩、省略默认值等多重优化,实现远超JSON/XML的传输效率。

这种结构化二进制容器的思想在网络协议、数据库存储等领域有着广泛应用。

本文介绍了Protobuf的原理,GRpc如何识别一个protobuffer结构, 以及varint技术。

目录

  • [1. 名称解析:Protocol Buffers 的由来](#1. 名称解析:Protocol Buffers 的由来 "#1-%E5%90%8D%E7%A7%B0%E8%A7%A3%E6%9E%90protocol-buffers%E7%9A%84%E7%94%B1%E6%9D%A5")
  • [2. 核心优化机制](#2. 核心优化机制 "#2-%E6%A0%B8%E5%BF%83%E4%BC%98%E5%8C%96%E6%9C%BA%E5%88%B6")
  • [3. 二进制编码详解](#3. 二进制编码详解 "#3-%E4%BA%8C%E8%BF%9B%E5%88%B6%E7%BC%96%E7%A0%81%E8%AF%A6%E8%A7%A3")
  • [4. 与其他技术的对比](#4. 与其他技术的对比 "#4-%E4%B8%8E%E5%85%B6%E4%BB%96%E6%8A%80%E6%9C%AF%E7%9A%84%E5%AF%B9%E6%AF%94")
  • [5. 类似模式的技术实践](#5. 类似模式的技术实践 "#5-%E7%B1%BB%E4%BC%BC%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%8A%80%E6%9C%AF%E5%AE%9E%E8%B7%B5")
  • [6. 设计哲学总结](#6. 设计哲学总结 "#6-%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6%E6%80%BB%E7%BB%93")
  • 附1 GRpc如何识别一个protobuffer结构
  • 附2 Varint压缩

1. 名称解析:Protocol Buffers 的由来

gRpc框架的序列化工具有一个名字,叫做protobuf, 又叫 protocol buffer, 我对这个名字一直觉得很奇怪,因此在这里拆解一下。

Protobuffer 就是 Protocol Buffer 的常用简称和昵称。

类似于:

JavaScript → JS

HyperText Markup Language → HTML

Portable Network Graphics → PNG

1.1 Protocol(协议)的含义

Protocol 指数据交换的预先约定,体现在 .proto 文件中定义的结构化数据契约:

protobuf 复制代码
message Person {
  string name = 1;    // 字段编号1
  int32 id = 2;       // 字段编号2
  string email = 3;   // 字段编号3
}

这种定义明确了数据格式、字段类型和编号规则,构成了通信双方必须遵守的协议规范。

1.2 Buffer(缓冲区)的含义

Buffer 并非传统意义的临时缓冲区,而是指序列化后的结构化二进制数据容器。它是一个精心设计的字节序列,承载着按照协议规范编码的完整数据单元。

2. 核心优化机制

2.1 字段编号替代字段名

通过用数字编号替换字符串字段名,大幅减少数据传输量:

  • 字段名 "name"(4字节)→ 编号 1(通常1字节)
  • 字段名 "email"(5字节)→ 编号 3(通常1字节)

name和email本身,因为proto定义已经是全局知晓这个结构的定义,所以

2.2 二进制编码优势

  • 无冗余字符:省略引号、括号、逗号等格式符号
  • 紧凑布局:数据连续排列,无填充对齐开销
  • 类型集成:字段编号与类型信息合并编码

2.3 Varint 可变长度整数

小数值占用更少字节:

  • 101(1字节)
  • 300AC 02(2字节)
  • 而非固定4字节存储

2.4 默认值省略

如果字段值为类型默认值(0、false、空字符串等),直接不传输该字段。

3. 二进制编码详解

3.1 实际编码示例

对于数据:{name: "Alice", id: 123}

Protocol Buffers 编码结果:

复制代码
0A 05 41 6C 69 63 65 10 7B

9个字节。

3.2 编码拆解分析

name字段部分 0A 05 41 6C 69 63 65

  • 0A = 00001010:前5位00001(字段编号1),后3位010(字符串类型),
  • 05:字符串长度5字节
  • 41 6C 69 63 65:"Alice"的ASCII码

id字段部分 10 7B

  • 10 = 00010000:后3位000(Varint类型),前5位00010(字段编号2)
  • 7B:十进制123的Varint编码

3.3 效率对比

相同数据的JSON需要约40字节,而Protocol Buffers仅需9字节,压缩比超过4:1。

4. 与其他技术的对比

特性 JSON/XML Protocol Buffers
数据格式 文本 二进制
字段传输 完整字段名 数字编号
元数据 引号、括号等
序列化大小
解析性能
可读性

5. 类似模式的技术实践

5.1 Apache Thrift

几乎相同的设计理念:

thrift 复制代码
struct Person {
  1: string name,
  2: i32 id,
  3: string email
}

5.2 网络协议设计

TCP/IP包头、HTTP/2帧结构等都采用类似的「固定格式二进制容器」设计。

5.3 数据库存储格式

MySQL行格式、列式存储等都使用预定义结构+二进制编码的模式。

5.4 多媒体容器

MP4、AVI等媒体格式使用类似的box/chunk结构承载编码数据。

6. 设计哲学总结

Protocol Buffers 的成功源于以下几个关键设计理念:

  1. 关注点分离:协议定义与具体实现解耦
  2. 效率优先:从编码层到传输层的全方位优化
  3. 兼容性设计:通过字段编号机制支持向前向后兼容
  4. 通用性:跨语言、跨平台的统一解决方案
  5. 工具链支持:代码生成、验证等完整生态

这种「协议定义+二进制容器」的模式之所以在计算机科学中广泛存在,是因为它完美平衡了效率、可维护性和扩展性,是构建高性能分布式系统的基石技术。

核心价值:Protocol Buffers 不是发明了新概念,而是将久经考验的二进制数据交换模式标准化、工具化,让开发者能够专注于业务逻辑而非数据传输细节。

附1 GRpc如何识别一个protobuffer结构

protobuffer本身对字段名进行了压缩,它怎么知道一个结构体是这个结构体?

Protobuf 在编码时去掉了字段名 (比如 username, user_id),这确实极大地压缩了数据。但它之所以能"知道"一个结构体是哪个结构体,关键在于它依赖的是一套预先严格定义好的"契约" ------也就是 .proto 文件。

这个过程可以分解为以下几个关键点:

1. 契约先行:.proto 文件是核心

所有使用 Protobuf 的通信双方(发送方和接收方)必须 在通信之前就拥有完全相同.proto 文件定义。

例如,我们定义一个用户消息:

protobuf 复制代码
// user.proto
message User {
  string user_name = 1;
  int64 favorite_number = 2;
}

这个 .proto 文件就是"契约"。它规定了:

  • 有一个叫做 User 的消息类型。
  • 这个类型有两个字段。
  • 每个字段都有一个唯一的数字标签 (1 和 2)和一个数据类型string, int64)。

2. 编码:用数字标签代替字段名

当你的程序要序列化一个 User 对象时(比如 user_name="Alice", favorite_number=42),Protobuf 编码器不会把字符串 "user_name""favorite_number" 写入二进制流。

它写入的是:

  • 字段1 :标签 1 + 值 "Alice"
  • 字段2 :标签 2 + 值 42

这些标签(1, 2)就是二进制数据中唯一标识字段的密钥

3. 解码:按图索骥

接收方拿到二进制数据后,用自己的 User 消息定义(即同一个 .proto 文件编译生成的代码)来解码。

解码过程是这样的:

  1. 读取一个字段的标签(比如 1)和它的数据类型。
  2. 在本地的 User 消息定义中查找:"哪个字段的标签是 1?"
  3. 找到了!标签 1 对应着 user_name 字段,类型是 string
  4. 于是,它正确地创建一个 User 对象,并将解码出的字符串 "Alice" 填入该对象的 user_name 属性。
  5. 继续读取下一个字段,重复此过程。

关键特性:灵活性、兼容性与未知字段

这种基于数字标签的机制带来了巨大的优势:

  • 向后/向前兼容 :如果接收方的 .proto 版本比较老,缺少发送方数据中的某些新字段(比如发送方用了 email = 3;),接收机在解码时看到不认识的标签(如 3),它会简单地忽略这个字段,而不会报错。这就是向前兼容。同样,老版本的客户端发送缺少新字段的数据给新版本的服务端,新服务端也能正常处理(向后兼容)。

  • 结构体类型的识别 :Protobuf 消息本身是自描述 的,但只描述到字段级别,不描述消息类型。那么,一个更根本的问题是:接收方如何知道这段二进制数据应该被解码成 User 消息,而不是 Product 消息?

    答案在于上层的应用程序协议。 Protobuf 通常被用作更高级别协议的数据载体。识别消息类型的方法通常有:

    • 包装在一个外层消息中:定义一个顶级的、包含所有可能消息类型的包装消息。
    protobuf 复制代码
    message TopLevelMessage {
      oneof message_type {
        User user = 1;
        Product product = 2;
        // ...
      }
    }

    解码时先解码 TopLevelMessage,根据其中的 oneof 字段判断具体类型。

    • 在协议头中指定:比如,在 gRPC 中,HTTP 请求路径就隐含了要调用的服务和方法,从而确定了消息类型。
    • 预先约定 :在简单的点对点通信中,双方可能就约定好"这个Socket连接上只发送 User 消息"。

总结

Protobuf 能知道一个结构体是哪个结构体,并不是靠二进制数据本身携带了类型名,而是依赖于:

  1. 通信双方共享的 .proto 契约:这是解码的"密码本"。
  2. 唯一的数字标签:这是二进制流中识别字段的"钥匙"。
  3. 上层的应用程序协议 :这负责告诉接收方"请使用 User.proto 这个密码本来解码接下来的数据"。

这种设计实现了数据的高度压缩,同时提供了出色的版本兼容性。

附2 Varint 压缩:小数字占用更少字节的编码艺术

在Proto Buffer中,我们提到过 Varint, 这里我做一个补充。

什么是 Varint?

Varint (Variable-length Integer,可变长度整数)是一种智能的整数编码技术,核心思想是:小数值占用更少字节,大数值才占用更多字节

思想类似于哈夫曼编码,但是不完全相等,哈夫曼编码基于频率,而Varint基于数字本身的大小。

传统整数的局限

在大多数系统中,整数类型有固定长度:

  • int32:固定 4 字节(32位)
  • int64:固定 8 字节(64位)

这意味着即使数值很小(比如数字 1),也要占用完整的 4 个字节,造成空间浪费。

Varint 编码原理

基本规则

  1. 每个字节只用 7 位存储数据,最高位(MSB)作为继续标志:

    • 0:表示这是最后一个字节
    • 1:表示后面还有更多字节
  2. 数据按小端序排列:低位字节在前

编码过程示例

例1:数字 1 的编码

scss 复制代码
二进制:00000001
Varint:00000001  (1字节)
  • 只有1个字节,最高位为 0

例2:数字 300 的编码

scss 复制代码
二进制:100101100 (300的二进制)
分组:  0000010   0101100  (按7位分组)
反转:  0101100   0000010  (小端序)
加标志:10101100  00000010 (最高位:1-继续, 0-结束)
十六进制:AC 02
  • 占用 2 字节:0xAC 0x02

例3:数字 1 的传统 vs Varint 对比

protobuf 复制代码
int32 value = 1;
  • 传统固定长度:00 00 00 01(4字节)
  • Varint 编码:01(1字节)
  • 节省 75% 空间!

Protocol Buffers 中的 Varint 应用

实际编码示例

protobuf 复制代码
message Test {
  int32 id = 1; // 含义:字段名为id的值为1
  ...
}

不同取值的编码结果:

python 复制代码
int32 id = 1      → 编码: 08 01          (2字节)
int32 id = 300    → 编码: 08 AC 02       (3字节)  
int32 id = 65536  → 编码: 08 80 80 04    (4字节)

08 01 = 0000 1000 0000 0001

0000 1000 前5位表示 字段序号,后3位表示字段类型,表示字段头,第一个字段,int类型

0000 0001 表示值为1。

字段头也是 Varint

字段编号和类型的组合也使用 Varint 编码:

python 复制代码
08 = 00001000  # 字段1,Varint类型

优势与局限

✅ 优势

  1. 空间高效:小数字占用极少空间
  2. 自描述:从数据本身可知长度
  3. 通用性强:适合各种整数类型

❌ 局限

  1. 大数代价:极大数字可能比固定长度更占空间
  2. 解析开销:需要动态解析字节数

这样将小负数转换为小正数,再利用 Varint 的高效编码。

实际影响

在真实业务数据中:

  • 大多数 ID、状态码、数量都是小数值
  • 90% 的整数可以用 1-2 个字节表示
  • 整体数据大小减少 30-50% 很常见

总结

Varint 压缩是 Protocol Buffers 高效性的关键技术之一,它通过「按需分配」的智能编码方式,在二进制级别实现了数据的最小化表示,特别适合网络传输和存储优化场景。

相关推荐
悟空码字1 小时前
手把手搭建Java微服务:从技术选型到生产部署
java·后端·微服务
leonardee1 小时前
MySQL----case的用法
java·后端
骑着bug的coder1 小时前
吃烤鱼时突然悟到的:为什么 Java 线程池的扩容逻辑是“反直觉”的?
后端
v***8572 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
武昌库里写JAVA2 小时前
Java如何快速入门?Java基础_Java入门
java·vue.js·spring boot·后端·sql
程序员爱钓鱼2 小时前
Python职业路线规划:从入门到高级开发者的成长指南
后端·python·trae
程序员爱钓鱼2 小时前
Python 编程实战 · 进阶与职业发展:自动化运维(Ansible、Fabric)
后端·python·trae
风的归宿553 小时前
gitlab配置ai代码审核
后端
格格步入3 小时前
线上问题:MySQL NULL值引发的投诉
后端·mysql