跳到主要内容

Protocol Buffers

Protocol Buffers(简称 protobuf)是 Google 开发的一种数据序列化格式,比 JSON 和 XML 更小、更快、更简单。它是 gRPC 的基础,用于定义服务接口和数据结构。

什么是 Protocol Buffers?

Protocol Buffers 是一种与语言无关、平台无关的可扩展机制,用于序列化结构化数据。它的主要优势在于:

特性Protocol BuffersJSONXML
数据大小小(二进制)中等(文本)大(文本)
解析速度中等
可读性需要解码
类型安全强类型弱类型弱类型
Schema必需可选可选(DTD/XSD)

Proto3 语法

gRPC 推荐使用 Proto3 语法,它比 Proto2 更简洁,支持更多语言。

基本结构

// 指定语法版本
syntax = "proto3";

// 包名,防止命名冲突
package mypackage;

// Go 语言的包路径
option go_package = "./mypackage";

// Java 的包名
option java_package = "com.example.mypackage";

// 消息定义
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}

字段编号

每个字段都有一个唯一的编号,用于在二进制格式中标识字段:

message Example {
string field1 = 1; // 编号 1
int32 field2 = 2; // 编号 2
bool field3 = 3; // 编号 3

// 编号 4-15 占用1字节
// 编号 16-2047 占用2字节
// 建议频繁使用的字段使用 1-15
}

重要规则

  • 编号 1-15 用一个字节编码,建议用于频繁使用的字段
  • 编号 19000-19999 保留给 Protocol Buffers 内部使用
  • 一旦字段被使用,编号不能改变(向后兼容)

数据类型

标量类型

message Scalars {
// 数值类型
double double_val = 1; // 64位浮点数
float float_val = 2; // 32位浮点数
int32 int32_val = 3; // 32位整数(负数效率低)
int64 int64_val = 4; // 64位整数(负数效率低)
uint32 uint32_val = 5; // 无符号32位整数
uint64 uint64_val = 6; // 无符号64位整数
sint32 sint32_val = 7; // 有符号32位整数(负数效率高)
sint64 sint64_val = 8; // 有符号64位整数(负数效率高)
fixed32 fixed32_val = 9; // 固定长度32位无符号
fixed64 fixed64_val = 10; // 固定长度64位无符号
sfixed32 sfixed32_val = 11; // 固定长度32位有符号
sfixed64 sfixed64_val = 12; // 固定长度64位有签名

// 布尔类型
bool bool_val = 13;

// 字符串类型
string string_val = 14; // UTF-8 编码字符串

// 字节类型
bytes bytes_val = 15; // 任意字节序列
}

类型选择建议

  • 普通整数:使用 int32int64
  • 可能为负的整数:使用 sint32sint64
  • 大正整数:使用 uint32uint64
  • 需要固定大小:使用 fixed32fixed64

枚举类型

enum Status {
UNKNOWN = 0; // 第一个值必须是0
ACTIVE = 1;
INACTIVE = 2;
DELETED = 3;
}

message User {
string name = 1;
Status status = 2;
}

枚举规则

  • 第一个枚举值必须为 0
  • 0 值是默认值
  • 可以使用 reserved 保留已删除的值
enum Status {
option allow_alias = true; // 允许别名
UNKNOWN = 0;
ACTIVE = 1;
ENABLED = 1; // 别名
reserved 2, 3, 4 to 10; // 保留的编号
reserved "DELETED", "BANNED"; // 保留的名称
}

复合类型

嵌套消息

message Address {
string street = 1;
string city = 2;
string country = 3;
}

message Person {
string name = 1;
Address address = 2; // 嵌套消息
}

// 或内部定义
message Company {
string name = 1;

message Department {
string name = 1;
int32 employee_count = 2;
}

Department department = 2;
}

数组(重复字段)

message Book {
string title = 1;
repeated string authors = 2; // 字符串数组
repeated int32 ratings = 3; // 整数数组
}

Map 类型

message Config {
map<string, string> settings = 1;
map<int32, double> scores = 2;
}

Map 注意事项

  • 不支持枚举类型作为 key
  • Map 是无序的
  • Map 不能是 repeated

默认值

Protocol Buffers 有默认值机制:

message Defaults {
int32 id = 1; // 默认值: 0
bool active = 2; // 默认值: false
string name = 3; // 默认值: ""(空字符串)
repeated int32 values = 4; // 默认值: [](空列表)
Status status = 5; // 默认值: UNKNOWN(第一个枚举值)
}

无法区分

  • 字段被设置为默认值
  • 字段未被设置

如需区分,可以使用 optional(Proto3)或包装类型:

import "google/protobuf/wrappers.proto";

message Example {
// 使用 optional
optional int32 id = 1; // 可以区分未设置

// 使用包装类型
google.protobuf.Int32Value count = 2;
google.protobuf.StringValue name = 3;
}

字段规则

message FieldRules {
// 单数字段(默认)
string single = 1;

// 可选字段(Proto3)
optional string optional_field = 2;

// 重复字段(数组)
repeated string repeated_field = 3;
}

保留字段

当删除字段时,应该保留编号防止重用:

message Person {
string name = 1;
int32 age = 2;

// 保留已删除的字段
reserved 3, 4;
reserved "old_field", "deleted_field";
}

服务定义

gRPC 服务在 proto 文件中定义:

syntax = "proto3";

package myservice;

// 定义服务
service Greeter {
// 一元 RPC:发送问候
rpc SayHello (HelloRequest) returns (HelloReply);

// 服务端流:返回多个问候
rpc SayHelloStream (HelloRequest) returns (stream HelloReply);

// 客户端流:发送多个请求
rpc SendGreetings (stream HelloRequest) returns (HelloReply);

// 双向流:双方都可以发送流
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

// 请求消息
message HelloRequest {
string name = 1;
}

// 响应消息
message HelloReply {
string message = 1;
}

导入其他 Proto 文件

// 导入标准 proto
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

// 导入自定义 proto
import "common.proto";

message Order {
string id = 1;
google.protobuf.Timestamp created_at = 2;
}

完整示例

// product.proto
syntax = "proto3";

package ecommerce;

option go_package = "./ecommerce";
option java_package = "com.example.ecommerce";

import "google/protobuf/timestamp.proto";

// 产品服务
service ProductService {
// 获取单个产品
rpc GetProduct (GetProductRequest) returns (Product);

// 列出产品(服务端流)
rpc ListProducts (ListProductsRequest) returns (stream Product);

// 创建订单(客户端流)
rpc CreateOrder (stream OrderItem) returns (Order);

// 实时库存更新(双向流)
rpc StreamInventory (stream InventoryUpdate) returns (stream InventoryNotification);
}

// 产品消息
message Product {
string id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock = 5;
Category category = 6;
google.protobuf.Timestamp created_at = 7;
}

// 产品分类
enum Category {
CATEGORY_UNKNOWN = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_FOOD = 3;
}

// 获取产品请求
message GetProductRequest {
string id = 1;
}

// 列出产品请求
message ListProductsRequest {
Category category = 1;
int32 page_size = 2;
string page_token = 3;
}

// 订单项
message OrderItem {
string product_id = 1;
int32 quantity = 2;
}

// 订单
message Order {
string id = 1;
repeated OrderItem items = 2;
double total = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}

// 订单状态
enum OrderStatus {
ORDER_STATUS_UNKNOWN = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
}

// 库存更新
message InventoryUpdate {
string product_id = 1;
int32 quantity_change = 2;
}

// 库存通知
message InventoryNotification {
string product_id = 1;
int32 current_stock = 2;
bool low_stock_alert = 3;
}

小结

本章我们学习了:

  1. Protocol Buffers 概念:二进制序列化格式的优势
  2. Proto3 语法:字段定义、数据类型、枚举
  3. 复合类型:嵌套消息、数组、Map
  4. 服务定义:gRPC 服务的 proto 定义
  5. 最佳实践:字段编号、保留字段、导入

下一章我们将深入学习如何定义 gRPC 服务。