UN5L5——Protobuf配置规则
UN5L5——Protobuf配置规则
本章代码关键字
1 | // 注释方式1 |
回顾自定义协议生成工具中的配置文件
我们在自定义协议配置工具相关知识点中,使用的是xml文件进行配置
我们只需要基于xml的规则,按照一定规则配置协议信息,之后获取xml数据用于生成代码文件
在Protobuf中原理是一样的,只不过Protobuf中有自己的配置规则,也自定义了对应的配置文件后缀格式
我们需要掌握Protobuf的配置规则,之后才能使用工具将其转为C#脚本文件
配置后缀
Protobuf中配置文件的后缀统一使用 .proto
,可以通过多个后缀为.proto
的配置文件进行配置
配置规则
这里记录的配置规则是使用proto3的,官网详细请看:Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
至于proto2语法,官网详细请看:Language Guide (proto 2) | Protocol Buffers Documentation (protobuf.dev)
-
注释方式
1
2
3// 注释方式1
/* 注释方式2
可多行注释*/ -
第一行版本号
决定了proto文档的版本号,如果不设置将默认是proto2
1
syntax = "proto3"; //决定了proto文档的版本号,如果不设置将默认是proto2
-
命名空间
1
package GamePlayerTest; //这决定了命名空间
-
消息类
1
2
3
4//消息类
message TestMsg {
//字段的声明在这里
} -
成员类型 和 唯一编号
Protobuf的成员声明示例:
成员类型 字段名 = 唯一编号;
注意!Protobuf中
=
右边不是字段的默认值,而是唯一编号,所有字段都必须要有一个唯一编号,
唯一编号在二进制流中标识字段,反序列化通过唯一编号来识别字段,将数据装载到类对应的变量中
例如:当编号为1的字段被注释后,即使传过来的数据包含编号为1的字段数据,该数据也不会被反序列化,而是被丢弃Protobuf有多种成员类型:
-
浮点数
float
,double
-
整数
-
变长编码
Protobuf在底层做了优化,变长编码会根据数字的大小,序列化时更少的字节数来存储
例如int32
,在C#中还是以int
类型存储的,但是序列化时可能会被优化为1或2个字节来存储数据-
一般变长编码整数(不适用于负数)
int32
,int64
——int
,long
-
适用于表示负数类型的整数
sint32
,sint64
——int
,long
-
变长编码无符号整数
uint32
,uint64
——uint
,ulong
-
-
定长编码
-
无符号的固定字节数的类型
它通常用来表示大于228或256次方的数,比
uint32
或uint64
更有效
fixed32
,fixed64
-
有符号的固定字节数的类型
sfixed32
,sfixed64
-
-
-
其他类型
- 布尔值:
bool
- 字符串:
string
- 字节字符串:
bytes
,这时Protobuf自己声明的字节字符串BytesString
,而不是C#的字节数组!
- 布尔值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30//消息类
message TestMsg {
//注意,= 右边的内容不是默认值,而是唯一编号,方便我们进行序列化与反序列化
//浮点数
float testF = 1; //C# - float
double testD = 2; //C# - double
//整数 - 变长编码,Protobuf底层做了优化,会根据数字的大小,序列化时更少的字节数来存储
//例如int32,在C#中还是以int类型存储的,但是序列化时可能会被优化为1或2个字节来存储数据
int32 testInt32 = 3; //C# - int(不太适用于表示负数,如要负数请使用sint32)
int64 testInt64 = 4; //C# - long(不太适用于表示负数,如要负数请使用sint64)
//适用于表示负数类型的整数 - 变长编码
sint32 testSInt32 = 5; //C# - int(适用于表示负数的整数)
sint64 testSInt64 = 6; //C# - long(适用于表示负数的整数)
//无符号整数 - 变长编码
uint32 testUInt32 = 7; //C# - uint(变长的编码)
uint64 testUIntt64 = 8; //C# - ulong(变长的编码)
//无符号的固定字节数的类型 - 它的字节不可变
fixed32 testFixed32 = 9; //C# - uint(它通常用来表示大于2的28次方的数,比uint32更有效)
fixed64 testFixed64 = 10; //C# - ulong(它通常用来表示大于2的56次方的数,比uint32更有效)
//有符号的固定字节数的类型 - 它的字节不可变
sfixed32 testSFix32 = 11; //C# - int
sfixed64 testSFix64 = 12; //C# - long
//其他类型
bool testBool = 13; //C# - bool
string testString = 14; //C# - string
bytes testBytes = 15; //C# - BytesString(Protobuf自己声明的字节字符串,不是C#的字节数组!)
} -
-
特殊标识
-
required
:必须赋值的字段,该语法不能用于proto3! -
optional
:可以不赋值的字段 -
repeated
:数组/List
-
map
:字典/Dictionary
1
2
3
4required float requiredF = 16; //必须赋值的字段
optional float optionalF = 17; //可以不赋值的字段
repeated int32 listInt = 18; //C# - 类似List<int>的使用
map<int32, string> testMap = 19; //C# - 类似Dictionary<int, string>的使用 -
-
枚举
枚举的声明和调用与C#很类似,但有几点需要注意的:
- 枚举的第一个常量字段必须映射到
0
!!! - 枚举的声明中每个常量字段语句使用
;
间隔,而不是,
- 在消息类中调用枚举时,
=
号右边写唯一编码而非默认值!
1
2
3
4
5
6
7
8
9
10
11
12
13
14syntax = "proto3"; //决定了proto文档的版本号,如果不设置将默认是proto2
package GamePlayerTest; //这决定了命名空间
//枚举的声明
enum TestEnum {
NORMAL = 0; //第一个常量必须映射到0
BOSS = 5; //后面的常量值可以随意
}
//消息类
message TestMsg {
TestEnum testEnum = 20; //枚举成员变量的声明,需要唯一编码
} - 枚举的第一个常量字段必须映射到
-
默认值
-
string
- 空字符串 -
bytes
- 空字节 -
bool
-false
- 数值 - 0
-
enum
- 0 -
message
- 取决于语言,C#为null
1
2
3
4
5
6
7
8message TestMsg2 {
int32 testInt32 = 1;
}
//消息类
message TestMsg {
TestMsg2 testMsg2 = 21; //声明自定义类对象,需要唯一编码,默认值是null
} -
-
允许嵌套
消息类和枚举的声明可以嵌套在另一个类声明中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//消息类
message TestMsg {
//嵌套一个类在另一个类当中,相当于是内部类
message TestMsg3 {
int32 testInt32 = 1;
}
//枚举的声明同样可以嵌套在内部
enum TestEnum2 {
NORMAL = 0; //第一个常量必须映射到0
BOSS = 1; //后面的常量值可以随意
}
TestMsg3 testMsg3 = 22;
TestEnum2 testEnum2 = 23;
} -
保留字段
如果修改了协议规则,删除了部分内容,为了避免更新时 重新使用 已经删除了的编号
我们可以利用reserved
关键字来保留字段,这些内容就不能再被使用了
之所以有这个功能,是为了在版本不匹配时,反序列化不会出现结构不统一,解析错误的问题例如,我们将编号25的
int32
字段注释后,如果远端protobuf文件版本不匹配的的情况下
则会发来的数据有可能会有编号25的int32
数据,此时如果该编号字段未被使用,我们可以不去解析编号25的数据
但如果编号25的字段又被重新使用且使用不同的类型,且发来的编号25数据还是int32
的,解析就会出现问题1
2
3
4
5message TestMsg {
int32 testInt24 = 24;
//int32 testInt25 = 25;
bool testBool25 = 25; //如果发来的数据是int32类型的,解析会出现问题
}因此,我们在注释某个编号字段时,同时使用
reserved
关键字来限制该编号,使得该编号无法在被使用1
2
3
4
5
6
7
8//消息类
message TestMsg {
//int32 testInt25 = 25;
reserved 25; //限制编号25的使用
reserved "testInt25"; //限制变量名testInt25的使用
reserved 100, 101, 102; //限制多个编号使用
reserved 110 to 120; //限制某个范围的编号使用
} -
导入定义
import "配置文件路径";
,如果你在某一个配置中 使用了另一个配置的类型,则需要导入另一个配置文件名假设要导入
HeartMsg
1
2
3
4
5syntax = "proto3";
package GameSystemTest;
message HeartMsg {
int64 time = 1;
}首先需要
import
HeartMsg
所在的配置文件,同路径下可以只填文件名,否则需要填入路径
调用文件外部的类时,需要加上其package
的名字,或者说命名空间名1
2
3
4
5
6
7
8syntax = "proto3"; //决定了proto文档的版本号,如果不设置将默认是proto2
package GamePlayerTest; //这决定了命名空间
import "test2.proto"; //如果在同路径,就可以直接填入文件名,否则需要填入路径
//消息类
message TestMsg {
GameSystemTest.HeartMsg testHeart = 26; //如果要使用另一个文件声明的消息类,必须要加上命名空间
}