UN5L5——Protobuf配置规则

本章代码关键字

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
31
32
33
34
35
36
37
38
39
40
// 注释方式1
/* 注释方式2
可多行注释*/
syntax = "proto3"; //决定了proto文档的版本号,如果不设置将默认是proto2
package namespaceName //设置命名空间
message messageName { } //声明消息类
//字段说明示例
成员类型 字段名 = 唯一编号;
//成员类型
//浮点数
float //C# - float
double //C# - double
//变长编码整数
int32 //C# - int(不太适用于表示负数,如要负数请使用sint32)
int64 //C# - long(不太适用于表示负数,如要负数请使用sint64)
//适用于表示负数类型的变长编码整数
sint32 //C# - int(适用于表示负数的整数)
sint64 //C# - long(适用于表示负数的整数)
//变长编码无符号整数
uint32 //C# - uint(变长的编码)
uint64 //C# - ulong(变长的编码)
//无符号的固定字节整数
fixed32 //C# - uint(它通常用来表示大于2的28次方的数,比uint32更有效)
fixed64 //C# - ulong(它通常用来表示大于2的56次方的数,比uint32更有效)
//有符号的固定字节整数
sfixed32 //C# - int
sfixed64 //C# - long
//其他类型
bool //C# - bool
string //C# - string
bytes //C# - BytesString(Protobuf自己声明的字节字符串,不是C#的字节数组!)
//特殊标识
required //必须要赋值的字段,该语法不能用于proto3!
optional //可以不赋值的字段
repeated //数组/List
map<,> //字典/Dictionary
//其他关键字
enum enumName { } //声明枚举
reserved //保留字段,使得某个编号或者变量名后续不可使用
import //导入某个配置文件,使得本配置文件可以引用其声明的类或枚举

回顾自定义协议生成工具中的配置文件

我们在自定义协议配置工具相关知识点中,使用的是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. 注释方式

    1
    2
    3
    // 注释方式1
    /* 注释方式2
    可多行注释*/
  2. 第一行版本号

    决定了proto文档的版本号,如果不设置将默认是proto2

    1
    syntax = "proto3";          //决定了proto文档的版本号,如果不设置将默认是proto2
  3. 命名空间

    1
    package GamePlayerTest;     //这决定了命名空间
  4. 消息类

    1
    2
    3
    4
    //消息类
    message TestMsg {
    //字段的声明在这里
    }
  5. 成员类型 和 唯一编号

    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#的字节数组!)
    }
  6. 特殊标识

    1. required​:必须赋值的字段,该语法不能用于proto3!
    2. optional​:可以不赋值的字段
    3. repeated​:数组/List
    4. map​:字典/Dictionary
    1
    2
    3
    4
    required float requiredF = 16;      //必须赋值的字段
    optional float optionalF = 17; //可以不赋值的字段
    repeated int32 listInt = 18; //C# - 类似List<int>的使用
    map<int32, string> testMap = 19; //C# - 类似Dictionary<int, string>的使用
  7. 枚举

    枚举的声明和调用与C#很类似,但有几点需要注意的:

    • 枚举的第一个常量字段必须映射到0​!!!
    • 枚举的声明中每个常量字段语句使用;间隔,而不是,
    • 在消息类中调用枚举时,=​号右边写唯一编码而非默认值!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    syntax = "proto3";          //决定了proto文档的版本号,如果不设置将默认是proto2

    package GamePlayerTest; //这决定了命名空间

    //枚举的声明
    enum TestEnum {
    NORMAL = 0; //第一个常量必须映射到0
    BOSS = 5; //后面的常量值可以随意
    }

    //消息类
    message TestMsg {
    TestEnum testEnum = 20; //枚举成员变量的声明,需要唯一编码
    }
  8. 默认值

    • string​ - 空字符串
    • bytes​ - 空字节
    • bool​ - false
    • 数值 - 0
    • enum​ - 0
    • message​ - 取决于语言,C#为null
    1
    2
    3
    4
    5
    6
    7
    8
    message TestMsg2 {
    int32 testInt32 = 1;
    }

    //消息类
    message TestMsg {
    TestMsg2 testMsg2 = 21; //声明自定义类对象,需要唯一编码,默认值是null
    }
  9. 允许嵌套

    消息类和枚举的声明可以嵌套在另一个类声明中

    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;
    }
  10. 保留字段

    如果修改了协议规则,删除了部分内容,为了避免更新时 重新使用 已经删除了的编号
    我们可以利用 reserved​ 关键字来保留字段,这些内容就不能再被使用了
    之所以有这个功能,是为了在版本不匹配时,反序列化不会出现结构不统一,解析错误的问题

    例如,我们将编号25的int32​字段注释后,如果远端protobuf文件版本不匹配的的情况下
    则会发来的数据有可能会有编号25的int32​数据,此时如果该编号字段未被使用,我们可以不去解析编号25的数据
    但如果编号25的字段又被重新使用且使用不同的类型,且发来的编号25数据还是int32​的,解析就会出现问题

    1
    2
    3
    4
    5
    message 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; //限制某个范围的编号使用
    }
  11. 导入定义

    import "配置文件路径";​,如果你在某一个配置中 使用了另一个配置的类型,则需要导入另一个配置文件名

    假设要导入HeartMsg

    1
    2
    3
    4
    5
    syntax = "proto3";
    package GameSystemTest;
    message HeartMsg {
    int64 time = 1;
    }

    首先需要importHeartMsg​所在的配置文件,同路径下可以只填文件名,否则需要填入路径
    调用文件外部的类时,需要加上其package​的名字,或者说命名空间名

    1
    2
    3
    4
    5
    6
    7
    8
    syntax = "proto3";          //决定了proto文档的版本号,如果不设置将默认是proto2
    package GamePlayerTest; //这决定了命名空间
    import "test2.proto"; //如果在同路径,就可以直接填入文件名,否则需要填入路径

    //消息类
    message TestMsg {
    GameSystemTest.HeartMsg testHeart = 26; //如果要使用另一个文件声明的消息类,必须要加上命名空间
    }