相关资料
代码地址: 代码中提供了常用情况的代码样例 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);