📖 第17课:Protobuf定义

阶段四:gRPC

Protocol Buffers(Protobuf)是gRPC的基石——它既是接口定义语言(IDL),也是高效的二进制序列化格式。Proto文件定义了服务的契约,自动生成的代码保证类型安全。本课深入Proto3语法、字段类型、消息设计模式、编码原理和最佳实践。

📝 Proto3语法详解

基本结构

// proto文件基本结构
syntax = "proto3";           // 指定版本(proto3是当前推荐版本)

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

option go_package = "./pb";  // Go代码生成选项
option java_package = "com.example.ecommerce";  // Java选项

// 导入其他proto文件
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

// 服务定义
service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  rpc CreateOrder(CreateOrderRequest) returns (Order);
}

// 消息定义
message Order {
  int32 id = 1;                                    // 字段编号
  string order_number = 2;                         // 字符串
  google.protobuf.Timestamp created_at = 3;        // 使用Well-Known类型
  OrderStatus status = 4;                          // 枚举
  repeated OrderItem items = 5;                    // 重复字段(数组)
  map<string, string> metadata = 6;                // Map
  oneof payment {                                   // Oneof(互斥字段)
    CreditCardPayment credit_card = 7;
    WechatPayment wechat = 8;
    AlipayPayment alipay = 9;
  }
}

标量类型

Proto类型Java类型Go类型说明
doubledoublefloat64双精度浮点
floatfloatfloat32单精度浮点
int32intint32变长编码,负数效率低
int64longint64变长编码
sint32intint32ZigZag编码,负数高效
sint64longint64ZigZag编码
uint32intuint32无符号变长
uint64longuint64无符号变长
fixed32intuint32定长4字节,值>2^28高效
fixed64longuint64定长8字节
boolbooleanbool布尔
stringStringstringUTF-8字符串
bytesByteString[]byte字节数组

枚举

enum OrderStatus {
  // proto3第一个枚举值必须是0(默认值)
  ORDER_STATUS_UNSPECIFIED = 0;  // 推荐:用前缀+UNSPECIFIED作默认值
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_PROCESSING = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
  ORDER_STATUS_CANCELLED = 5;
}

// ⚠️ 枚举值不要复用已删除的编号!
// 如果删除了OLD_STATUS = 3,不要把新枚举设为3
// 使用reserved保留
enum Color {
  reserved 2, 3;           // 保留编号
  reserved "RED", "BLUE";  // 保留名称
  COLOR_UNSPECIFIED = 0;
  COLOR_GREEN = 1;
  COLOR_YELLOW = 4;
}

repeated和map

message Product {
  int32 id = 1;
  string name = 2;
  
  // repeated = 数组/列表
  repeated string tags = 3;            // string数组
  repeated ProductImage images = 4;    // 对象数组
  
  // map = 键值对
  map<string, string> attributes = 5;  // 属性映射
  map<int32, DiscountRule> rules = 6;  // 编号→规则
  
  // ⚠️ map的key不能是float/bytes/enum/message
  // ⚠️ repeated不能嵌套map(repeated map<...>不允许)
}

message ProductImage {
  string url = 1;
  int32 width = 2;
  int32 height = 3;
}

message DiscountRule {
  double discount = 1;
  string description = 2;
}

oneof——互斥字段

// oneof:同一时刻只能设置其中一个字段
message Payment {
  int32 id = 1;
  double amount = 2;
  
  oneof method {
    CreditCardPayment credit_card = 3;
    WechatPayment wechat = 4;
    AlipayPayment alipay = 5;
    BankTransfer bank_transfer = 6;
  }
}

message CreditCardPayment {
  string card_number = 1;  // 实际应加密
  string expiry = 2;
}

message WechatPayment {
  string open_id = 1;
  string transaction_id = 2;
}

message AlipayPayment {
  string user_id = 1;
  string trade_no = 2;
}

message BankTransfer {
  string bank_name = 1;
  string account = 2;
}

嵌套和引用

// 嵌套消息
message Order {
  int32 id = 1;
  
  // 嵌套定义
  message Address {
    string street = 1;
    string city = 2;
    string province = 3;
    string zip_code = 4;
  }
  
  Address shipping_address = 2;
  Address billing_address = 3;
  
  // 外部引用嵌套类型: Order.Address
}

// Well-Known类型(google/protobuf/)
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/any.proto";
import "google/protobuf/wrappers.proto";

message EnhancedOrder {
  int32 id = 1;
  google.protobuf.Timestamp created_at = 2;   // 精确时间
  google.protobuf.Duration processing_time = 3; // 持续时间
  google.protobuf.Any extra = 4;                // 任意类型
  google.protobuf.Int32Value priority = 5;      // 可空int32(wrapper)
}

🛠️ 实战:电商订单Proto完整定义

// ecommerce.proto - 完整电商订单Proto
syntax = "proto3";

package ecommerce;

option go_package = "./pb";

import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";

// ===== 枚举 =====
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_PROCESSING = 3;
  ORDER_STATUS_SHIPPED = 4;
  ORDER_STATUS_DELIVERED = 5;
  ORDER_STATUS_CANCELLED = 6;
  ORDER_STATUS_REFUNDED = 7;
}

enum PaymentStatus {
  PAYMENT_STATUS_UNSPECIFIED = 0;
  PAYMENT_STATUS_PENDING = 1;
  PAYMENT_STATUS_PAID = 2;
  PAYMENT_STATUS_FAILED = 3;
  PAYMENT_STATUS_REFUNDED = 4;
}

// ===== 核心消息 =====
message Order {
  int32 id = 1;
  string order_number = 2;
  int32 user_id = 3;
  OrderStatus status = 4;
  PaymentStatus payment_status = 5;
  
  repeated OrderItem items = 6;
  ShippingInfo shipping = 7;
  PaymentInfo payment = 8;
  
  double subtotal = 9;     // 商品小计
  double discount = 10;    // 折扣金额
  double shipping_fee = 11; // 运费
  double total = 12;       // 总计
  
  string note = 13;
  map<string, string> metadata = 14;
  
  google.protobuf.Timestamp created_at = 15;
  google.protobuf.Timestamp updated_at = 16;
}

message OrderItem {
  int32 id = 1;
  int32 product_id = 2;
  string product_name = 3;
  string sku = 4;
  double unit_price = 5;
  int32 quantity = 6;
  double subtotal = 7;
  map<string, string> attributes = 8;  // 颜色/尺码等
}

message ShippingInfo {
  string recipient = 1;
  string phone = 2;
  Address address = 3;
  string carrier = 4;
  string tracking_number = 5;
  google.protobuf.Timestamp shipped_at = 6;
  google.protobuf.Timestamp delivered_at = 7;
}

message Address {
  string province = 1;
  string city = 2;
  string district = 3;
  string street = 4;
  string zip_code = 5;
}

message PaymentInfo {
  string payment_method = 1;
  string transaction_id = 2;
  double amount = 3;
  google.protobuf.Timestamp paid_at = 4;
}

// ===== 请求/响应 =====
message GetOrderRequest { int32 id = 1; }
message ListOrdersRequest {
  int32 user_id = 1;
  OrderStatus status = 2;
  int32 page = 3;
  int32 page_size = 4;
}
message ListOrdersResponse {
  repeated Order orders = 1;
  int32 total = 2;
}
message CreateOrderRequest {
  int32 user_id = 1;
  repeated CreateOrderItem items = 2;
  ShippingInfo shipping = 3;
  string payment_method = 4;
  string note = 5;
  string coupon_code = 6;
}
message CreateOrderItem {
  int32 product_id = 1;
  int32 quantity = 2;
  map<string, string> attributes = 3;
}
message UpdateOrderRequest {
  int32 id = 1;
  OrderStatus status = 2;
  string note = 3;
  google.protobuf.FieldMask update_mask = 4;  // 部分更新
}
message CancelOrderRequest {
  int32 id = 1;
  string reason = 2;
}

// ===== 服务定义 =====
service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc UpdateOrder(UpdateOrderRequest) returns (Order);
  rpc CancelOrder(CancelOrderRequest) returns (Order);
}

service ProductQueryService {
  rpc GetProduct(GetProductRequest) returns (Product);
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
}

message Product {
  int32 id = 1;
  string name = 2;
  string category = 3;
  double price = 4;
  int32 stock = 5;
  repeated string tags = 6;
}

message GetProductRequest { int32 id = 1; }
message ListProductsRequest {
  string category = 1;
  string search = 2;
  int32 page = 3;
  int32 page_size = 4;
}
message ListProductsResponse {
  repeated Product products = 1;
  int32 total = 2;
}

📏 Protobuf编码原理

// Protobuf编码:Varint + Tag-Value

// 字段编号 + 类型 → Tag
// Tag = (field_number << 3) | wire_type
// wire_type: 0=Varint, 1=64-bit, 2=Length-delimited, 5=32-bit

// 示例:int32 id = 1;  值=150
// Tag = (1 << 3) | 0 = 0x08
// Varint(150) = 0x9601
// 编码结果: 08 96 01 (3字节)

// 对比JSON: {"id":150} = 9字节
// Protobuf节省了67%的空间!

// 字段编号选择:
// 1-15: 1字节Tag(高频字段务必使用)
// 16-2047: 2字节Tag
// ⚠️ 19000-19999保留给Protobuf内部使用

message CompactMessage {
  int32 high_freq_field = 1;    // 1字节Tag ✅
  string another_high = 2;      // 1字节Tag ✅
  bool flag = 3;                // 1字节Tag ✅
  int32 low_freq = 16;          // 2字节Tag(可接受)
  string very_low = 1000;       // 2字节Tag(低频可以)
}

📝 本课小结

  1. Proto3是Protobuf当前推荐版本,语法简洁
  2. 标量类型选择:负数用sint32/sint64,大正数用fixed32/fixed64
  3. 枚举第一个值必须是0,推荐UNSPECIFIED命名
  4. repeated=数组,map=字典,oneof=互斥
  5. Well-Known类型(Timestamp/Duration/Any/FieldMask)提供标准能力
  6. 字段编号1-15最紧凑,高频字段务必使用
  7. reserved保留已删除的编号和名称
  8. 二进制编码比JSON节省60-80%空间

💪 练习

练习1:设计社交网络Proto

为社交网络定义Proto:用户、帖子、评论、点赞、关注、消息。注意oneof(消息类型)和枚举设计。

练习2:验证Protobuf编解码

用Node.js验证Protobuf的编解码,对比JSON和Protobuf的大小差异。

const protobuf = require('protobufjs');
const root = protobuf.loadSync('ecommerce.proto');
const Order = root.lookupType('ecommerce.Order');
const payload = { id: 1, orderNumber: "ORD-001", status: "ORDER_STATUS_PENDING" };
const msg = Order.create(payload);
const buffer = Order.encode(msg).finish();
const jsonSize = JSON.stringify(payload).length;
console.log(`JSON: ${jsonSize} bytes, Protobuf: ${buffer.length} bytes`);

🏆 本课成就:Proto架构师