Protocol Buffers (.proto) 协议设计规范与语法详解

设计一个符合Protocol Buffers语法规则的.proto文件。以下是一个完整的指南,包括语法规则、常用字段类型、选项等,并给出一个物联网传感器数据的示例。

一、Protocol Buffers语法规则

1.1 文件结构

  • 以.proto作为文件扩展名

  • 使用proto2或proto3语法版本(推荐使用proto3,更简洁)

  • 文件开头使用syntax关键字指定版本

  • 使用package定义包名(避免命名冲突)

  • 使用message定义消息结构

  • 使用service定义RPC服务(如果需要)

1.2 消息定义

  • 消息由字段组成,每个字段有唯一的编号(从1开始)

  • 字段编号用于二进制编码,一旦定义不应更改

  • 字段规则(proto2):

    • required:必须提供(proto3已取消)

    • optional:可选字段

    • repeated:数组或列表

  • 字段类型:标量类型(如int32, float, bool, string, bytes)或复合类型(其他message)

消息(Message)定义规则

protobuf

复制代码
// 使用驼峰命名法
message SensorData {
  // 字段规则:required, optional, repeated(proto3只有optional和repeated)
  optional uint32 device_id = 1;      // proto2语法
  uint32 timestamp = 1;               // proto3默认为optional
  repeated float readings = 2;        // 数组/列表
  
  // 字段编号:1-15使用1字节编码,16-2047使用2字节
  // 编号一旦使用不应更改
  reserved 5, 9 to 11;                // 保留字段编号
  reserved "old_field", "deprecated"; // 保留字段名
  
  // 嵌套消息
  message Location {
    double latitude = 1;
    double longitude = 2;
  }
  
  // 使用嵌套消息
  optional Location location = 3;
}

1.3 字段编号

  • 1到15:单字节编码(适用于频繁使用的字段)

  • 16到2047:双字节编码

  • 最大字段编号为2^29-1(536,870,911),其中19000到19999为保留编号

1.4 保留字段

  • 使用reserved关键字保留字段编号或字段名,避免被意外使用

1.5 枚举

  • 使用enum定义枚举类型

  • 枚举值必须从0开始

  • 0值为默认值,因此通常将第一个枚举值设为0

复制代码
// 使用大写下划线命名
enum DeviceType {
  DEVICE_TYPE_UNKNOWN = 0;      // 0值必须存在,作为默认值
  DEVICE_TYPE_SENSOR = 1;
  DEVICE_TYPE_ACTUATOR = 2;
  DEVICE_TYPE_GATEWAY = 3;
  DEVICE_TYPE_CONTROLLER = 4;
}

// 使用前缀避免命名冲突
enum SensorType {
  SENSOR_TEMPERATURE = 0;
  SENSOR_HUMIDITY = 1;
  SENSOR_PRESSURE = 2;
  SENSOR_LIGHT = 3;
  
  // 别名(需要开启allow_alias选项)
  option allow_alias = true;
  SENSOR_TEMP = 0;  // 与SENSOR_TEMPERATURE相同
}

1.6 注释

  • 使用双斜线//进行单行注释

  • 使用/* ... */进行多行注释

二、常用标量类型

.proto类型 C/C++类型 描述
float float 32位浮点数
double double 64位浮点数
int32 int32_t 使用变长编码,负数编码效率低(如果可能有负数,使用sint32)
int64 int64_t 同上
uint32 uint32_t 无符号32位整数,变长编码
uint64 uint64_t 无符号64位整数,变长编码
sint32 int32_t 有符号32位整数,变长编码,负数编码效率高
sint64 int64_t 有符号64位整数,变长编码,负数编码效率高
fixed32 uint32_t 总是4字节,如果值大于2^28则比uint32更高效
fixed64 uint64_t 总是8字节,如果值大于2^56则比uint64更高效
sfixed32 int32_t 总是4字节
sfixed64 int64_t 总是8字节
bool bool 布尔类型
string std::string 必须是UTF-8编码或7位ASCII文本
bytes std::string 任意字节序列

标量类型映射表

protobuf

复制代码
message ScalarTypes {
  // 整数类型
  int32 int_field = 1;      // 变长编码,负数效率低
  sint32 sint_field = 2;    // 变长编码,负数效率高
  sfixed32 fixed_int = 3;   // 固定4字节
  
  int64 long_field = 4;
  sint64 slong_field = 5;
  sfixed64 fixed_long = 6;
  
  uint32 uint_field = 7;    // 无符号
  uint64 ulong_field = 8;
  fixed32 fixed_uint = 9;
  fixed64 fixed_ulong = 10;
  
  // 浮点数
  float float_field = 11;   // 32位
  double double_field = 12; // 64位
  
  // 布尔
  bool bool_field = 13;
  
  // 字符串和字节
  string str_field = 14;    // 必须是UTF-8
  bytes bytes_field = 15;   // 任意二进制数据
  
  // 枚举
  enum Status {
    UNKNOWN = 0;            // 枚举必须从0开始
    ACTIVE = 1;
    INACTIVE = 2;
    ERROR = 3;
  }
  Status status = 16;
}

三、Nanopb特定选项

Nanopb为嵌入式系统提供了一些扩展选项,可以在.proto文件中指定,以控制生成代码的行为。

  • (nanopb).max_size:指定bytes或string字段的最大长度

  • (nanopb).max_count:指定repeated字段的最大数量

  • (nanopb).fixed_length:将bytes字段设置为固定长度

  • (nanopb).fixed_count:将repeated字段设置为固定数量

四、物联网传感器数据协议示例

下面是一个完整的物联网传感器数据采集和控制的.proto文件示例,包括请求和响应消息。

protobuf

复制代码
// 指定使用proto3语法
syntax = "proto3";

// 指定包名,避免命名冲突
package iot_protocol;

// 引入nanopb选项(如果需要)
import "nanopb.proto";

// 设备状态枚举
enum DeviceStatus {
    DEVICE_UNKNOWN = 0;
    DEVICE_OK = 1;
    DEVICE_ERROR = 2;
    DEVICE_CALIBRATING = 3;
    DEVICE_SLEEPING = 4;
}

// 传感器类型枚举
enum SensorType {
    SENSOR_TEMPERATURE = 0;
    SENSOR_HUMIDITY = 1;
    SENSOR_PRESSURE = 2;
    SENSOR_LIGHT = 3;
    SENSOR_ACCELEROMETER = 4;
}

// 单个传感器数据
message SensorData {
    SensorType type = 1;                // 传感器类型
    float value = 2;                    // 传感器数值
    uint32 timestamp = 3;               // 时间戳(单位:秒)
    uint32 error_code = 4;              // 错误码,0表示无错误
}

// 设备信息
message DeviceInfo {
    string device_id = 1;                // 设备ID,最长32字节
    string firmware_version = 2;         // 固件版本,最长16字节
    uint32 battery_level = 3;            // 电池电量(百分比)
    DeviceStatus status = 4;             // 设备状态
    repeated SensorType supported_sensors = 5;  // 支持的传感器类型列表
}

// 遥测数据包(设备主动上报)
message TelemetryPacket {
    DeviceInfo device_info = 1;          // 设备信息
    repeated SensorData sensors = 2;     // 传感器数据数组
    uint32 packet_id = 3;                // 包ID,用于去重和排序
    uint32 interval = 4;                 // 当前采样间隔(单位:秒)
}

// 命令类型枚举
enum CommandType {
    CMD_SET_INTERVAL = 0;                // 设置采样间隔
    CMD_REQUEST_DATA = 1;                // 请求立即上报数据
    CMD_CALIBRATE = 2;                   // 校准传感器
    CMD_REBOOT = 3;                      // 重启设备
    CMD_UPDATE_CONFIG = 4;               // 更新配置
}

// 设备命令(云端->设备)
message DeviceCommand {
    CommandType type = 1;                // 命令类型
    uint32 command_id = 2;               // 命令ID,用于对应响应
    oneof payload {
        uint32 interval = 3;             // 用于CMD_SET_INTERVAL,设置间隔(秒)
        string config_json = 4;          // 用于CMD_UPDATE_CONFIG,配置信息(JSON格式)
    }
}

// 命令响应(设备->云端)
message CommandResponse {
    uint32 command_id = 1;               // 对应的命令ID
    bool success = 2;                     // 是否成功执行
    string message = 3;                   // 响应消息,错误时包含错误信息
    DeviceStatus device_status = 4;       // 执行命令后的设备状态
}

// 服务定义(如果需要RPC)
service DeviceService {
    // 设备上报遥测数据(单向)
    rpc ReportTelemetry(TelemetryPacket) returns (Empty) {}
    
    // 云端发送命令,设备响应
    rpc SendCommand(DeviceCommand) returns (CommandResponse) {}
    
    // 设备请求配置(可选)
    rpc RequestConfig(DeviceInfo) returns (DeviceConfig) {}
}

// 一些辅助消息
message Empty {}
message DeviceConfig {
    // 配置字段,根据需要定义
}

嵌套和导入

protobuf

复制代码
// 在common.proto中定义基础类型
message Header {
  uint32 version = 1;
  uint32 sequence = 2;
  uint32 timestamp = 3;
  bytes session_id = 4 [(nanopb).max_size = 16];
}

// 在主proto文件中引用
message TelemetryPacket {
  common.Header header = 1;  // 使用导入的消息
  
  // oneof:互斥字段,同一时间只能设置一个
  oneof data {
    SensorData sensor = 2;
    EventData event = 3;
    CommandResponse response = 4;
  }
  
  // map类型(proto3)
  map<string, string> metadata = 5;
  
  // 任意类型(需要导入any.proto)
  google.protobuf.Any extension = 6;
}

五、Nanopb选项的使用

在上面的示例中,我们可以为嵌入式系统添加Nanopb选项,以控制生成代码的内存分配。例如:

protobuf

复制代码
message DeviceInfo {
    string device_id = 1 [(nanopb).max_size = 32];                // 设备ID,最长32字节
    string firmware_version = 2 [(nanopb).max_size = 16];         // 固件版本,最长16字节
    // ... 其他字段
    repeated SensorType supported_sensors = 5 [(nanopb).max_count = 10];  // 最多支持10种传感器
}

这样,生成的代码会使用固定大小的数组,避免动态内存分配。

六、注意事项

  1. 字段编号一旦分配,不应更改,因为编码后的消息使用字段编号来标识字段。

  2. 不要使用保留的字段编号(19000到19999)。

  3. 考虑向前和向后兼容性:

    • 不要删除已有的字段,可以标记为reserved或添加optional(proto2)以保持兼容。

    • 新添加的字段应该是可选的或具有合理的默认值。

  4. 对于嵌入式系统,尽量使用固定大小的字段(如fixed32fixed64)以避免变长编码的开销(如果值经常很大)。

  5. 使用repeated字段时,考虑设置max_count以限制数组大小,便于内存分配。

通过遵循这些规则,你可以设计出高效且可扩展的.proto文件,适用于嵌入式物联网系统。

命名规范

protobuf

复制代码
// 文件命名:小写+下划线,以.proto结尾
// sensor_data.proto, device_management.proto

// 消息命名:驼峰式,首字母大写
// SensorData, DeviceStatus, CommandRequest

// 字段命名:小写+下划线
// device_id, sensor_type, battery_level

// 枚举命名:大写+下划线,使用前缀
// DEVICE_TYPE_SENSOR, SENSOR_STATUS_ACTIVE

// 服务命名(RPC):驼峰式
// DeviceService, TelemetryCollector
相关推荐
小湘西1 天前
UML 用例图图中包含和扩展区别
uml·设计规范·设计语言
Andy Dennis7 天前
一文漫谈面向对象编程中的SOLID编程
java·软件工程·设计规范
摸摸电8 天前
锁存器、触发器、寄存器区别
单片机·嵌入式硬件·设计规范
摸摸电8 天前
阻抗反射系数计算
嵌入式硬件·设计规范
摸摸电8 天前
DRAM结构
单片机·嵌入式硬件·设计规范
我真的是大笨蛋9 天前
MVCC解析
java·数据库·spring boot·sql·mysql·设计模式·设计规范
首席拯救HMI官16 天前
【拯救HMI】AR技术与HMI融合:工业现场的可视化新范式
网络·stm32·单片机·网络协议·ar·设计规范
我命由我1234517 天前
图像格式:RGB、BGR、RGBA、BGRA
图像处理·经验分享·笔记·学习·学习方法·photoshop·设计规范
brave and determined22 天前
工程设计类学习(DAY6):PCB可制造性设计规范全解析
网络·嵌入式硬件·学习·pcb设计·设计规范·嵌入式设计·设计工艺