Skip to content

什么是 Protobuf ​

在网络通信和通用数据交换等应用场景中经常使用的技术是特殊格式的字符串(文本格式的 JSON 或 XML)

网络通信和通用数据交换除了字符串还可以使用二进制数据流(比如上传/下载文件 二进制格式的)

Protobuf: 全称 Protocol Buffers 是由 Google 开发的一种语言无关、平台无关、可扩展的 序列化结构化数据的方法 它主要用于数据存储(如持久化)和通信协议(如 RPC 框架 gRPC 的默认数据格式)

其核心思想是: 通过一种独立的接口定义语言(即 .proto 文件)来描述数据结构, 然后使用编译器(protoc)生成目标编程语言 (如 Go, Rust, Python, JavaScript 等)的源代码, 开发者使用这些生成的代码, 可以方便地序列化和反序列化结构化数据

protobuf 基本使用步骤:

  1. 编写 .proto 文件: 定义数据结构
  2. 编译 .proto 文件: 使用编译器将 .proto 文件 编译为目标语言的代码
  3. 在目标语言代码中使用生成的结果文件

快速开始 ​

1.编写 .proto 文件 ​

  • 1.创建文件: touch example.proto
  • 2.编辑文件内容如下:
proto
// 指定 protobuf 的版本, proto3 是最新的语法版本
syntax = "proto3";

// 使用 message 关键字定义数据结构
// 可以理解为 Go 语言中的struct/JS 中的 object/Python 中的 dict
message Response {
  string results = 1;   // 定义一个 string 类型的字段, 字段名字为 results, 序号为1
  int32  status  = 2;   // 定义一个 int32  类型的字段, 字段名字为 status,  序号为2
}

// 注意:
// 0.双斜线表示注释
// 1.序号不是编程语言的值(可以理解为它是字段的唯一 ID)
// 2.不能重复(唯一ID,肯定是不能重复的)
// 3.不可变性(定义了就不能修改)

2.将 .proto 文件编译为目标语言的代码 ​

2.1 安装目标语言对应的 proto 编译器 ​

它就是一个命令软件, 安装的方式有很多, 最直接的方式就是手动下载并安装

sh
# for macOS
brew install protobuf

# for Windows winget
winget install -e --id Google.Protobuf

安装之后, 打开终端模拟器输入: protoc --version 能够正确输出版本说明没有问题

2.2 编译为目标代码示例 ​

[注意]

protoc 并不负责序列化, protoc 只负责生成代码

真正的序列化(将对象转为字节)是由 protoc 生成的代码中

调用的底层目标语言(C++/Rust/Go/Java)运行时库完成的

sh
# >>>注意1: 还需要安装用于 go 对象序列化插件: protoc-gen-go 和 protoc-gen-go-rpc
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
#
# >>>注意2: 生成 Go 代码需要设置 option go_package 这么一个选项如:
# option go_package = "github.com/gh_username/go_demo/mypkg/protobufs";
# 否则就会生成失败, 报错信息如下:
# protoc-gen-go: unable to determine Go import path for "example.proto"
#
# Please specify either:
#         • a "go_package" option in the .proto source file, or
#         • a "M" argument on the command line.
#
# See https://protobuf.dev/reference/go/go-generated#package for more information.
# --go_out: protoc-gen-go: Plugin failed with status code 1.
# 原因是因为所有 Go 源代码必须指定包名
#
# >>> 注意3:  --go_opt=paths=source_relative 是用来控制生成的结果文件的目录结构
# paths=source_relative 表示相对当前命令执行的相对目录
#
# >>> 注意4: protoc --go_out=. 表示将生成的结果(xxx.pb.go)保存到当前目录下

protoc --go_out=. --go_opt=paths=source_relative ./example.proto

# 如果执行成功, 会生成一个 example.pb.go 文件
# 打开这个文件会发现, 其实它的内容就是 go 语言的代码
sh
# >>> 注意1: 需要安装生成 JS/TS 代码的插件
# npm i -g @bufbuild/protoc-gen-es
#
# >>> 注意2: --es_opt=target=js 表示生成 js代码/也可以 target=ts 直接生成 ts 代码
# --plugin=protoc-gen-es: 表示用这个 protoc-gen-es 命令来生成目标语言代码

protoc  --es_out=. --es_opt=target=js --plugin=protoc-gen-es  ./example.proto
protoc  --es_out=. --es_opt=target=ts --plugin=protoc-gen-es  ./example.proto

# 会生成JS/TS代码文件: example_pb.js/example_pb.ts
# js 还可以动态加载 proto 文件然后编译(但是在运行时编译会损失一部分性能)
sh
protoc --python_out=. example.proto

# 成功执行会生成一个 example_pb2.py 文件
# 其中的内容就是 python 代码
sh
protoc --java_out=. ./example.proto
# 生成 Java 代码: Example.java

3.在编程语言中用到生成的源码 ​

静态语言和解释语言(脚本语言)用法最大的区别就是: 是否可以在运行时直接编译 proto 文件

go
package main

// 1. 安装(example.pb.go源码需要的依赖): go get -u google.golang.org/protobuf/proto
// 2. 在 main.go 中使用 example.pb.go 测试, 包括文件中定义的 struct 和方法

import (
	// 1. 导入 example.pb.go
	"fmt"
	"log"

	"github.com/gh_username/go_demo/mypkg/protobufs"
	"google.golang.org/protobuf/proto"
	// 2. 导入 protobuf 核心运行时库
)

func main() {
	// 由于 go 语言是通过 package 来查找(变量/结构体)等信息的所以一定要注意包名路径
	// 1. example.proto 中的 option go_pakcage 选项
	// 2. go 源码项目的目录结构(go_demo)
	// 3. 项目文件目录树如下:
	// ├── go.mod   ->    module github.com/gh_username/go_demo
	// ├── go.sum
	// ├── main.go
	// └── mypkg
	//     └── protobufs
	//         ├── example.pb.go
	//         └── example.proto

	// 1.使用 example.pb.go 中的结构体
	res := &protobufs.Response{
		Status:  11,            // 注意字段名必须是 大写 的(大写才能公开访问)
		Results: "Hello World", // 注意字段的值需要符合 proto 中定义的类型
	}

	// 2.使用 Getter 方法(example.pb.go会未每个字段实现 getter)
	fmt.Printf("Status = %v, Results = %v \n", res.Status, res.Results)
	fmt.Printf("Status = %v, Results = %v \n", res.GetStatus(), res.GetResults())

	// 3.encode: 将 go 结构体转换为二进制字节流(protobuf)
	bytes, err := proto.Marshal(res)
	if err != nil {
		log.Fatalf("序列化失败: %v", err)
	}
	fmt.Printf("序列化后的字节长度: %d\n", len(bytes))
	fmt.Printf("序列化后的字节内容: %v\n", bytes)

	// 4.decode: 将二进制字节流(protobuf)解析为 go 语言结构体
	var newRes protobufs.Response
	if err := proto.Unmarshal(bytes, &newRes); err != nil {
		log.Fatalf("反序列化失败: %v", err)
	}
	fmt.Printf("反序列化结果: Status = %v, Results = %v\n", res.GetStatus(), res.GetResults())
}
js
import { ResponseSchema } from "./example_pb.js"; // 注意: 是 ESM 模块化规范

// 注意: 需要安装依赖用来序列化/反序列化
// npm i @bufbuild/protobuf
import { create, toBinary, fromBinary } from "@bufbuild/protobuf";

// 1. 使用 ResponseSchema 创建对象并给字段赋值
const resp = create(ResponseSchema);
resp.status = 1001;
resp.results = "message from main.js";
console.log("respObject Instance:", resp);

// 2. 序列化: JS 对象转 Protobuf 二进制
const bytes = toBinary(ResponseSchema, resp);
console.log("JS对象转Protobuf", bytes);

// 3. 反序列化: Protobuf 二进制转 JS 对象
const respObject = fromBinary(ResponseSchema, bytes);
console.log("Protobuf 转 JS 对象", respObject);
js
import protobufjs from "protobufjs";

// 注: 如果是动态加载 proto 文件并使用 protobufjs 转译
// 那么就不需要用 protoc 将 example.proto 转译为 example_pb.proto

(async () => {
  // 1. 动态加载 .proto 文件
  const root = await protobufjs.load("./example.proto");

  // 2. 查找指定的 Message 类型
  // 参数必须包含 package 名和 message 名, 格式为 "packageName.MessageName"
  const RespType = root.lookupType("example.Response");

  // 3. 准备数据
  const data = {
    status: 1002,
    results: "hello from test.js",
  };

  // 4. 验证数据是否符合 .proto 定义的约束
  const verifyError = RespType.verify(data);
  if (verifyError) {
    throw new Error(`数据验证失败: ${verifyError}`);
  }

  // 5. 创建 Message 实例
  const respInst = RespType.create(data);
  console.log("JS 对象 respInst:", respInst);

  // 6. 序列化: JS实例对象转 protobuf 二进制流程
  // encode() 返回 Writer, 调用 finish() 获取最终的 Uint8Array
  const buffer = RespType.encode(respInst).finish();
  console.log("序列化后的 Buffer 数据:", buffer);

  // 7. 反序列化: 将 protobuf 二进制流转 JS 实例对象
  // decode() 接收 Uint8Array, 返回解析后的 Message 实例
  const respObj = RespType.decode(buffer);
  console.log("反序列化结果(Message):", respObj);
  console.log("转换为普通对象(Object):", respObj.toJSON());
})();

Protobuf 语法 ​

在 快速开始 中已经知道如何定义 message

proto
syntax = "proto3"; // 声明语法版本

message 消息名 {    // {}整个称之为消息体
  字段类型 字段序号
}

基本类型 ​

也可以叫做标量类型Scalar Types 是指一些基本的数据类型, 可以直接在 .proto 文件中定义并用于数据字段 这些类型对应于原生的简单数据类型,类似于编程语言中的整型、浮点型、布尔型等

proto
syntax = "proto3"; // 声明语法版本
message ScalarData {
  double a   = 1;
  float b    = 2;
  int32 c    = 3;
  int64 d    = 4;
  uint32 e   = 5;
  uint64 f   = 6;
  sint32 g   = 7;
  sint64 h   = 8;
  fixed32 i  = 9;
  fixed64 j  = 10;
  sfixed32 k = 11;
  sfixed64 l = 12;
  bool m     = 14;
  string n   = 15;
  bytes o    = 16;
}

复杂类型 ​

枚举 ​

proto
syntax = "proto3";

enum Month{
  // 枚举的第一个值必须为0, 后续的值可以是不连续的
  Jan = 0;
  Feb = 1;
  Mar = 2;
  Apr = 3;
  May = 4;
  Jun = 5;
  Jul = 6;
  Aug = 7;
  Sep = 8;
  Oct = 9;
  Nov = 10;
  Dec = 11;
  Unknown = 404; // 可以不连续
}

message Msg1 {
  Month month = 1; // 枚举类型的字段
}

重复字段 ​

表示一个字段可以出现n次, 可以理解为编程语言中的列表/数组

proto
syntax = "proto3";

message User {
  int32 id = 1;               // 用户id  number
  string nickname = 2;        // 用户昵称 string
  repeated string emails = 3; // 用户邮箱 Array<string>
}

映射 ​

key-value 对, 可以理解为编程语言中的字典(dict)/映射(map)

proto
syntax = "proto3";

message User {
  int32 id = 1;               // 用户id  number
  string nickname = 2;        // 用户昵称 string
  map<string, string> infos = 3; // 用户额外信息(如:身高/体重/年龄) Map<string, string>
}

任意类型 ​

proto
syntax = "proto3";

message Response {
  int32 errno = 1;                 // 响应错误码
  string message = 2;              // 响应消息
  google.protobuf.Any results = 3; // 响应结果数据
}

消息嵌套 ​

proto
syntax = "proto3";

message User {
  int32  id = 1;       // 用户唯一id
  string nickname = 2; // 用户昵称
}

message Article {
  int32  id = 1;    // 文章唯一id
  string title = 2; // 文章标题
  User author = 3;  // 文章作者信息(使用 User 消息类型)
}

模块化 ​

如果项目比较大, 如果将所有的消息结构都定义到一个文件中不利于维护, 可以使用 import 语法来导入其他文件中定义的 message

proto
syntax = "proto3";

message User {
  int32  id = 1;       // 用户唯一id
  string nickname = 2; // 用户昵称
}
proto
syntax = "proto3";
import "./user.proto"; // 导入其他 proto

message Article {
  int32  id = 1;    // 文章唯一id
  string title = 2; // 文章标题
  User author = 3;  // 文章作者信息
}

package ​

  1. 与 go 语言中的 package 作用非常类似
  2. 与 go 语言不同的是 package 并不是强制需要设置的(默认可以不设置)
  3. 设置 package 后, import 语句需要指定 package 名(防止重名)
proto
syntax = "proto3";
package article;
import "./user.proto"; // 导入其他 proto

message Article {
  int32  id = 1;        // 文章唯一id
  string title = 2;     // 文章标题
  foo.User author = 3;  // 文章作者信息
}
proto
syntax = "proto3";
package foo;

message User {
  int32  id = 1;       // 用户唯一id
  string nickname = 2; // 用户昵称
}

其中之一 oneof ​

这个东西有点类似 C 语言中联合体 union 的概念: 多个字段在同一时刻只能有一个被赋值

proto
syntax = "proto3";

message User {
  int32 id = 1;          // 用户id
  oneof contact_method { // 联系方式有一种既可
    string phone = 2;
    string email = 3;
    string wechat = 4;
  }
}

定义服务 service ​

proto
syntax = "proto3";

message Article {
  int32 id = 1;        // 文章id
  int32 title = 2;     // 文章标题
  string contents = 3; // 文章内容
  int32 stars = 4;     // 文章收藏数
  int32 likes = 5;     // 文章点赞数
  int32 shares = 6;    // 文章分享数
}

// 文章列表接口请求参数(数据结构)
message ArticleServiceListRequest {
  int32 page = 1;  // 页码
  int32 limit = 2; // 每页多少条数据
}

// 文章列表接口响应参数(数据结构)
message ArticleServiceListResponse {
  int32 count = 1;            // 数据库数据总条数
  repeated Article items = 2; // 当前页数据 JS: Array<Article>
}

// 用于描述要生成的目标语言的远程过程调用方法
service ArticleService {
  // rpc 方法名(参数类型) 返回类型
  rpc List(ArticleServiceListRequest) returns (ArticleServiceListResponse);
}

配置选项 option ​

用于控制 protoc 的行为

  • 比如生成 Java 代码时, 是否要生成多个 class
  • 比如生成 Go 代码时, 如何设置 go_package 字段
  • 配置选项有很多, 建议查看官方文档

常见的选项有

proto
syntax = "proto3";

// 设置 go_package 选项, 注意区分 go 语言 package 和 proto package
option go_package = "github.com/gh_username/go_demo/your_pkg/ptbfs";
option java_package = "com.google.protobuf";
option java_outer_classname = "DescriptorProtos";
option csharp_namespace = "Google.Protobuf.Reflection";
option objc_class_prefix = "GPB";
option cc_enable_arenas = true;
option optimize_for = SPEED;

Released under the MIT License.