2、Protobuf基础语法:定义.proto文件、消息类型、字段规则、字段编号
好,咱们正式开始动手写 Protobuf 了。
这一章我带你过一遍 .proto 文件的基础语法。说白了,这就是 Protobuf 的「身份证」——你定义好数据结构,它才能帮你生成代码。我在做物联网项目时,经常看到新手一上来就写字段,结果后面改得想哭。所以,咱们先把规矩讲清楚。
2.1 .proto 文件长什么样?
一个 .proto 文件,本质上就是一个文本文件。你用记事本都能写。但它的结构非常严格。
我习惯把 .proto 文件放在项目根目录下的 proto/ 文件夹里。比如这样:
syntax = "proto3"; // 指定语法版本,目前主流是 proto3
package sensor_data; // 包名,防止命名冲突
message TemperatureReading {
float value = 1;
string unit = 2;
int64 timestamp = 3;
}
嗯,这里要注意:syntax 必须写在第一行,不写的话默认是 proto2。我个人强烈建议你用 proto3,它去掉了不少坑爹的规则,后面我会讲到。
2.2 消息类型(Message)
消息类型是 Protobuf 的核心。你可以把它理解成 C 语言里的 struct,或者 JSON 里的一个对象。
定义消息用 message 关键字,后面跟消息名字。名字建议用大驼峰命名法,比如 MyMessage、DeviceStatus。
核心原则:一个 .proto 文件里可以定义多个 message,甚至可以嵌套。但别搞得太复杂,我见过有人一个文件塞了 50 个 message,维护起来简直是噩梦。
举个例子,一个传感器节点可能包含多个数据:
message SensorNode {
string node_id = 1;
float temperature = 2;
float humidity = 3;
bool is_online = 4;
}
你看,每个字段都有类型、名字和编号。类型可以是 int32、float、string、bool 等基础类型,也可以是另一个 message 类型。
2.3 字段规则:required / optional / repeated
这是新手最容易踩坑的地方。咱们一个一个说。
2.3.1 required(proto2 才有,proto3 已废弃)
在 proto2 里,required 表示这个字段必须存在,不能为空。听起来很美好对吧?
但我曾经在一个项目中吃过它的亏。当时我们定义了一个 required 字段,后来业务变更,这个字段在某些场景下不需要了。结果一升级,所有旧数据都解析失败,线上直接崩了。
所以 proto3 直接砍掉了 required。现在所有字段默认都是可选的。我个人觉得这是最明智的决定之一。
2.3.2 optional(proto3 中的显式可选)
proto3 里虽然没有 required,但你可以显式声明 optional。它的作用是告诉编译器:这个字段可能有值,也可能没有。当你读取时,可以用 has_xxx() 方法判断。
message DeviceConfig {
optional int32 baud_rate = 1; // 波特率,可能没设置
string device_name = 2; // 默认就是 optional
}
说实话,我平时很少用 optional,因为 proto3 默认所有字段都是 optional 的。只有在需要区分「字段没设置」和「字段设置了默认值」时,我才会显式加上。
2.3.3 repeated(列表/数组)
repeated 表示这个字段可以重复多次,说白了就是数组或列表。这是物联网场景里最常用的规则。
message SensorHistory {
repeated float temperature_samples = 1; // 温度采样列表
repeated int64 sample_times = 2; // 对应的时间戳
}
我在做环境监测项目时,一个节点每 10 秒上报一次数据,一天下来就是 8640 条。用 repeated 打包发送,比一条一条发效率高太多了。
小技巧:repeated 字段在 proto3 中默认使用 packed 编码,也就是把所有值紧凑地打包在一起。这能节省不少带宽,尤其是在传输大量整数或浮点数时。
2.4 字段编号(Field Number)
每个字段都必须有一个唯一的编号。这个编号不是随便写的,它直接影响序列化后的字节大小。
编号范围是这样的:
| 编号范围 | 说明 | 占用字节 |
|---|---|---|
| 1 - 15 | 最常用字段,推荐使用 | 1 字节 |
| 16 - 2047 | 不常用字段 | 2 字节 |
| 19000 - 19999 | 保留,不可使用 | - |
| 大于 2^29 - 1 | 保留,不可使用 | - |
为什么会这样?因为 Protobuf 用 Varint 编码编号,1-15 只需要 1 个字节,16 以上就需要 2 个字节了。你想想看,如果一个消息有 100 个字段,每个都用编号 1000,那光编号就占 200 字节,太浪费了。
我的建议:
- 把最常用的字段放在 1-15 号
- 把扩展字段、不常用的放在 16 号以后
- 永远不要修改已有字段的编号
- 删除字段时,用
reserved关键字保留编号,防止别人误用
message MyMessage {
reserved 2, 15, 9 to 11; // 这些编号已被删除,不可再用
reserved "old_field", "deprecated_field"; // 这些字段名也被保留
string new_field = 1;
int32 another_field = 3;
}
避坑指南:我曾经在升级协议时,直接删除了一个字段,然后新加了一个字段用了同样的编号。结果旧设备解析新数据时,把新字段的值当成了旧字段,数据全乱了。所以,删除字段一定要用 reserved 把编号和名字都锁死。
2.5 一个完整的例子
最后,我给你看一个我在智能家居项目中用过的 .proto 文件片段。它包含了我们刚才讲的所有知识点:
syntax = "proto3";
package smart_home;
message LightStatus {
bool is_on = 1; // 常用字段,编号 1
int32 brightness = 2; // 亮度 0-100
string color = 3; // 颜色值,如 "#FF0000"
reserved 4; // 之前有个 mode 字段,已废弃
}
message DeviceReport {
string device_id = 1;
repeated LightStatus lights = 2; // 多个灯的状态
float battery_level = 16; // 不常用,放 16 号
int32 signal_strength = 17; // 信号强度,也不常用
}
你看,我把 device_id 和 lights 放在了 1-15 号,因为它们每次上报都会出现。而 battery_level 和 signal_strength 可能隔几次才上报一次,所以放在 16 号以后,省带宽。
嗯,这一章的内容就这些。说白了,.proto 文件就是一张蓝图,你把结构画清楚,后面生成代码、序列化、反序列化就都顺了。下一章咱们就动手编译这个 .proto 文件,看看它到底能生成什么代码。