Protocol Buffers
Protocol Buffers(简称 protobuf)是 Google 开发的一种数据序列化格式,比 JSON 和 XML 更小、更快、更简单。它是 gRPC 的基础,用于定义服务接口和数据结构。
什么是 Protocol Buffers?
Protocol Buffers 是一种与语言无关、平台无关的可扩展机制,用于序列化结构化数据。它的主要优势在于:
| 特性 | Protocol Buffers | JSON | XML |
|---|---|---|---|
| 数据大小 | 小(二进制) | 中等(文本) | 大(文本) |
| 解析速度 | 快 | 中等 | 慢 |
| 可读性 | 需要解码 | 高 | 高 |
| 类型安全 | 强类型 | 弱类型 | 弱类型 |
| 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; // 任意字节序列
}
类型选择建议:
- 普通整数:使用
int32或int64 - 可能为负的整数:使用
sint32或sint64 - 大正整数:使用
uint32或uint64 - 需要固定大小:使用
fixed32或fixed64
枚举类型
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;
}
小结
本章我们学习了:
- Protocol Buffers 概念:二进制序列化格式的优势
- Proto3 语法:字段定义、数据类型、枚举
- 复合类型:嵌套消息、数组、Map
- 服务定义:gRPC 服务的 proto 定义
- 最佳实践:字段编号、保留字段、导入
下一章我们将深入学习如何定义 gRPC 服务。