ProtoBuf 使用分享(下)

2020/07/14 ProtoBuf

相关资料

代码地址: 代码中提供了常用情况的代码样例 java 实现 (推荐一个idea protobuf工具:protocol-buffer-editor 可以进行 代码与protobuf文件的跳转 等功能)
    https://github.com/viakiba/viakiba/tree/master/proto_demo/prototest
资料来源:
    https://developers.google.com/protocol-buffers/docs/proto3
    https://stackoverflow.com/questions/21159451/protobuffs-import-from-another-directory
    https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/#%E6%9B%B4%E6%96%B0%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B

高级使用

消息嵌套

消息嵌套 支持 消息内嵌套,消息外同文件多个消息以及import导入其他文件

具体实现见代码:
    https://github.com/viakiba/viakiba/tree/master/proto_demo/prototest
建议阅读:
    https://www.tizi365.com/archives/380.html
消息生成:
    https://blog.viakiba.cn/2020/07/14/protobuf-%E6%8E%A2%E7%9F%A5(%E4%B8%8A)/#消息生成

oneOf

如果你有一组字段,同时最多允许这一组中的一个字段出现,就可以使用Oneof定义这一组字段。

因为proto3没有办法区分正常的值是否是设置了还是取得缺省值(比如int64类型字段,如果它的值是0,你无法判断数据是否包含这个字段,因为0几可能是数据中设置的值,也可能是这个字段的零值) 所以你可以通过Oneof取得这个功能,因为Oneof有判断字段是否设置的功能。

syntax = "proto3";
package abc;
message OneofMessage {
    oneof testOneof {
        string name = 1;
        int64 value = 2;
    }
}

更新字段类型

  • 不要改变已有字段的字段编号
  • 把单一一个值改变成一个新的oneof类型的一个成员是安全和二进制兼容的。把一组字段变成一个新的oneof字段也是安全的,如果你确保这一组字段最多只会设置一个。把一个字段移动到一个已存在的oneof字段是不安全的
新增字段
当你增加一个新的字段的时候,老系统序列化后的数据依然可以被你的新的格式所解析,只不过你需要处理新加字段的缺省值。 老系统也能解析你信息的值,新加字段只不过被丢弃了
修改字段类型
  • int32, uint32, int64, uint64 和 bool类型都是兼容的
  • sint32 和 sint64兼容,但是不和其它整数类型兼容
  • string 和 bytes兼容,如果 bytes 是合法的UTF-8 bytes的话
  • 嵌入类型和bytes兼容,如果bytes包含一个消息的编码版本的话
  • fixed32和sfixed32, fixed64和sfixed64
  • enum和int32, uint32, int64, uint64格式兼容
删除字段
  • 只要字段编号不会在更新的消息类型中再次使用,就可以删除字段。 您可能想要重命名该字段,例如添加前缀“Obsolete_”,或者保留字段编号,这样.proto的未来用户就不会意外地重复使用该编号。

any

Any字段允许你处理嵌套数据,并不需要它的proto定义。一个Any以bytes呈现序列化的消息,并且包含一个URL作为这个类型的唯一标识和元数据。 为了使用Any类型,你需要引入google/protobuf/any.proto。

import "google/protobuf/any.proto";
message ErrorStatus {
    string message = 1;
    repeated google.protobuf.Any details = 2;
}

其他

JSON 映射

ProtoBuf 可以进行 json 编码 ,映射参考:

https://developers.google.com/protocol-buffers/docs/proto3#json
添加依赖
 <dependency>
     <groupId>com.googlecode.protobuf-java-format</groupId>
     <artifactId>protobuf-java-format</artifactId>
     <version>1.2</version>
 </dependency>  
代码实现
 // protobuf 转 json
 Message.Builder message = Message.newBuilder();
 String json = JsonFormat.printToString(message.build());
 // json 转 protobuf
 JsonFormat.merge(json, message);

Option

  • java_package
    • 默认情况下将使用proto包(在.proto文件中使用“package”关键字指定)。 package 通常不会生成友好的Java包,因为package 指定生成的包不会以反向域名开始。 如果生成的不是Java代码,则此选项无效。
    • option java_package = “com.example.foo”;
  • java_multiple_files
    • 使消息、枚举和服务在包级别定义,而不是在以.proto文件命名的外部类中定义。
    • option java_multiple_files = true;
  • java_outer_classname
    • 要生成的Java类的类名(因此也就是文件名)。 如果在.proto文件中没有指定显式的java_outer_classname,将通过将.proto文件名转换为驼峰大小写(因此foo_bar.proto变为FooBar.java)来构造类名。 如果不生成Java代码,则此选项无效。
    • option java_outer_classname = “Ponycopter”;
  • deprecated …………………. 还有很多 每个语言都有自己的 option

service

ProtoBuf 可以搭配 GRPC 使用,生成 GRPC 服务接口.

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

消息生成工具

每个消息定义文件都是需要一个一个生成,规则很简单,索性来个递归给生成了,下面是 python 的实现

import os

# proto中的 import 导入 要根据 projectPath 的设置进行制定 
# 假如文件是 D:\\PersonCode\\viakiba\\proto_demo\\prototest\\message\\filedemo\\adress.proto
# proto_path (-I) 参数是 projectPath (D:\\PersonCode\\viakiba\\proto_demo\\prototest\\)
# 则 import 要写 import "message/filedemo/adress.proto"; 拼接上就是绝对路径

projectPath = 'D:\\PersonCode\\viakiba\\proto_demo\\prototest\\'
javaOutPath = 'D:\\PersonCode\\viakiba\\proto_demo\\prototest\\src\\main\\java\\'
pythonOutPath = 'D:\\PersonCode\\viakiba\\proto_demo\\prototest\\python\\message'

allpath=[]
allname=[]

def getallfile(path):
    allfilelist=os.listdir(path)
    # 遍历该文件夹下的所有目录或者文件
    for file in allfilelist:
        filepath=os.path.join(path,file)
        # 如果是文件夹,递归调用函数
        if os.path.isdir(filepath):
            getallfile(filepath)
        # 如果不是文件夹,保存文件路径及文件名
        elif os.path.isfile(filepath):
            allpath.append(filepath)
            allname.append(file)
    return allpath, allname

# protoc -I=projectPath --java_out=javaOutPath filePath
# protoc -I=D:\\PersonCode\\viakiba\\proto_demo\\prototest\\ --java_out=D:\\PersonCode\\viakiba\\proto_demo\\prototest\\src\\main\\java\\   D:\\PersonCode\\viakiba\\proto_demo\\prototest\\message\\filedemo\\adress.proto
if __name__ == "__main__":
    rootdir = "D:\\PersonCode\\viakiba\\proto_demo\\prototest\\message"
    files, names = getallfile(rootdir)
    for file in files:
        cmdJava = "protoc -I=" + projectPath + " --java_out=" + javaOutPath + " " + file
        cmdPython = "protoc -I=" + projectPath + " --python_out=" + pythonOutPath + " " + file
        print(cmdJava)
        print(cmdPython)
        os.system(cmdJava)
        os.system(cmdPython)

思考

protobuf 提供了消息的多平台生成,在游戏开发中消息生成大多都有自己的生成工具,但是大多繁杂需要写模板写抽象类,模板还需要各种类型判断与引用其他结构。我所见到的是xml文件配置写的,虽然不是很万能但是还是ok的,但是工具的实现很复杂。protobuf免除了这个工具的实现成本。只需要使用类似xml的配置文件,给每个消息识别配上序号,生成对应的识别代码。就能结合 netty / mina 很轻松的网络层的传输实现。 例如

<xml>
    <model>
        <compoent>
            <messgae class="消息 类 全路径"></message>
            <messgae></message>
            <messgae></message>
            <messgae></message>
        </compoent>
    </model>
</xml>

为每一个model预留300个消息号,为每一个 compoent 预留 50 个消息好,按照顺序为每一个配置message的标记序号,生成map所在的文件就好了。这一步借助 thymeleaf / freemarker 都不是很麻烦。

Code

    // write
    String name = "hello_world";
    PersonMessage.Person.Builder builder = PersonMessage.Person.newBuilder();
    builder.addName(name);// name 属性
    HashMap<String, String> map = new HashMap<>();
    map.put("1","sad");
    builder.putAllBag(map);// bag 属性
    AdressOuterClass.Adress.Builder adress = AdressOuterClass.Adress.newBuilder();
    adress.setAdressName("长安街100号");
    builder.setAdress(adress);//Adress 属性
    PersonMessage.Sex.Builder sex = PersonMessage.Sex.getDefaultInstance().toBuilder();
    sex.setSexType(PersonMessage.SexType.woman);
    builder.setSex(sex.build());// sex 属性
    PersonMessage.Person.High defaultInstance = PersonMessage.Person.High.getDefaultInstance();
    PersonMessage.Person.High.Builder high = defaultInstance.newBuilderForType().setHeighNum(100);
    builder.setHigh(high);
    byte[] bytes = builder.build().toByteArray();
    System.out.println(Base64.getEncoder().encodeToString(bytes));
    //byte数组 可以在网路中 通过 TCP 进行传递 例如 socket
    // read
    PersonMessage.Person builderreader = PersonMessage.Person.parseFrom(bytes);
    String nameReader = builderreader.getName(0);
    // 输出 这是 protobuf 提供的 toString 工具 格式化输出 目前已经被标记为废弃 但是还能用
    System.out.println("-----------------------文本格式----------------------");
    String ret = TextFormat.printToString(builderreader);
    System.out.println(ret);
    System.out.println("-----------------------转为JSON格式----------------------");
    // 也可以使用 protobuf-java-util pom文件有依赖 这个可以使 输出json字符串
    String jsonStr = printer.print(builderreader);
    System.out.println(jsonStr);
    System.out.println("-----------------------name第一个值----------------------");
    System.out.println(nameReader);
    System.out.println("-----------------------sex属性----------------------");
    System.out.println(builderreader.getSex().getSexType().getNumber());
    System.out.println("-----------------------json转protobuf对象并输出字符串----------------------");
    // json 转 protobuf
    PersonMessage.Person.Builder newPerson = PersonMessage.Person.newBuilder();
    JsonFormat.parser().merge(jsonStr,newPerson);
    jsonStr = printer.print(builderreader);
    System.out.println(jsonStr);

Search

    Table of Contents