[关闭]
@phper 2020-06-12T18:03:54.000000Z 字数 9827 阅读 6107

ProtoBuf 语法学习笔记

上一节学习了protoc命令的用法,以及配合生成grpc的相关。这一节,来学习一下proto文件中的语法规则。

protoc 命令通过解析 *.proto文件,来生成对应语言的服务文件。

看一个例子:

  1. syntax = "proto3";
  2. package proto;
  3. option go_package = ".;proto";
  4. message User {
  5. string name=1;
  6. int32 age=2;
  7. }
  8. message Id {
  9. int32 uid=1;
  10. }
  11. //要生成server rpc代码
  12. service ServiceSearch{
  13. rpc SaveUser(User) returns (Id){}
  14. rpc UserInfo(Id) returns (User){}
  15. }

可以看出,它的语法结构有点像Makefile风格。

官网文档

https://developers.google.com/protocol-buffers/docs/proto3

官网文档很全,可以对应的学习。但是很多东西,可能并没有用到,全部学习一边,第一是很累,第二,新人上手,很容易受挫,所以,我直接跳过一些非常基础的,也跳过一些不常用的,本次就学一下常用的一些元素,直接入题。

0. 反人类

protobuf 是一个跨平台的结构数据序列化方法的工具,我们使用之前,得把我们接口里用到的所有数据,在proto文件里先定义出来

这意味着,我们得把接口输入输出的所有的数据字段,都得定义在proto文件里,这一点真的是蛋疼!

比如,我一个接口,吐出的数据是一张表,里面有100个字段,那么,就得先在proto文件里讲这100个字段,用message的方式给定义出来。

这。。。。

所以,难在如何把自己的实际业务接口数据抽象出来,匹配上protobuf的语法,我认为这个是最难的。

1. message

我们初学时,看到的proto文件的例子基本都是从mesage开始的。可以说,它是最基础的,也是90%用的最多的元素了。那么啥是message呢?字面翻译为消息,那么啥是消息呢?

在讲消息之前,我们先回忆一下,普通的接口输出Json格式的数据是啥样的,这样会更好的利于我们理解message:

  1. {
  2. "name": "james",
  3. "age": 18
  4. }

上面是我们调用/UserInfo?id=1接口,输出了用户的信息,是一个json格式的数据,里面有2个参数:姓名和年龄。

由于proto的特性,那么我们就得提前在proto文件里定义一下这2个元素:

  1. syntax = "proto3";
  2. message User {
  3. string name=1;
  4. int32 age=2;
  5. }

我们先不看这一段具体是啥意思,我们先分别用PHP和golang转换输出一下,看下转换后的语言结构是咋样的:

  1. protoc --php_out=:. hello.proto

先看PHP生成后的文件长啥样?它会自动在本目录下生成了2个文件夹:ProtoGPBMetadata

  1. ├── GPBMetadata
  2.    └── Hello.php
  3. ├── Proto
  4.    └── User.php
  5. ├── hello.proto

Hello.php的内容为:

  1. <?php
  2. # Generated by the protocol buffer compiler. DO NOT EDIT!
  3. # source: hello.proto
  4. namespace GPBMetadata;
  5. class Hello
  6. {
  7. public static $is_initialized = false;
  8. public static function initOnce() {
  9. $pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool();
  10. if (static::$is_initialized == true) {
  11. return;
  12. }
  13. $pool->internalAddGeneratedFile(hex2bin(
  14. "0a3f0a0b68656c6c6f2e70726f746f120570726f746f22210a0455736572" .
  15. "120c0a046e616d65180120012809120b0a03616765180220012805620670" .
  16. "726f746f33"
  17. ), true);
  18. static::$is_initialized = true;
  19. }
  20. }

咋一眼看,只知道是一个单例模式,具体也不清楚它是啥作用的。不过不要急,继续看User.php文件。

  1. <?php
  2. # Generated by the protocol buffer compiler. DO NOT EDIT!
  3. # source: hello.proto
  4. namespace Proto;
  5. use Google\Protobuf\Internal\GPBType;
  6. use Google\Protobuf\Internal\RepeatedField;
  7. use Google\Protobuf\Internal\GPBUtil;
  8. /**
  9. * Generated from protobuf message <code>proto.User</code>
  10. */
  11. class User extends \Google\Protobuf\Internal\Message
  12. {
  13. /**
  14. * Generated from protobuf field <code>string name = 1;</code>
  15. */
  16. protected $name = '';
  17. /**
  18. * Generated from protobuf field <code>int32 age = 2;</code>
  19. */
  20. protected $age = 0;
  21. /**
  22. * Constructor.
  23. *
  24. * @param array $data {
  25. * Optional. Data for populating the Message object.
  26. *
  27. * @type string $name
  28. * @type int $age
  29. * }
  30. */
  31. public function __construct($data = NULL) {
  32. \GPBMetadata\Hello::initOnce();
  33. parent::__construct($data);
  34. }
  35. /**
  36. * Generated from protobuf field <code>string name = 1;</code>
  37. * @return string
  38. */
  39. public function getName()
  40. {
  41. return $this->name;
  42. }
  43. /**
  44. * Generated from protobuf field <code>string name = 1;</code>
  45. * @param string $var
  46. * @return $this
  47. */
  48. public function setName($var)
  49. {
  50. GPBUtil::checkString($var, True);
  51. $this->name = $var;
  52. return $this;
  53. }
  54. /**
  55. * Generated from protobuf field <code>int32 age = 2;</code>
  56. * @return int
  57. */
  58. public function getAge()
  59. {
  60. return $this->age;
  61. }
  62. /**
  63. * Generated from protobuf field <code>int32 age = 2;</code>
  64. * @param int $var
  65. * @return $this
  66. */
  67. public function setAge($var)
  68. {
  69. GPBUtil::checkInt32($var);
  70. $this->age = $var;
  71. return $this;
  72. }
  73. }

这个php文件里面的生成的几个方法很清晰,方别是采用链式结构设置和读取类的成员变量 age 的值。看来,在PHP里面,protobuf的处理方式是用面向对象的方式来处理数据。1个message,就会生成1个类,这个message里的每一个字段,就会变成php对象里的成员属性。然后再自动生成相同数量的getset方法,来获取和设置每一个成员熟悉的值。嗯。看上去虽然有点繁琐,但是基本也能看的懂。

我们接着看下,golang里面,自动生成的文件是怎样的。

  1. protoc --go_out=:. hello.proto

会在本目录下,生成1个hello.pb.go的文件:

  1. // Code generated by protoc-gen-go. DO NOT EDIT.
  2. // versions:
  3. // protoc-gen-go v1.21.0
  4. // protoc v3.11.3
  5. // source: hello.proto
  6. package proto
  7. import (
  8. proto "github.com/golang/protobuf/proto"
  9. protoreflect "google.golang.org/protobuf/reflect/protoreflect"
  10. protoimpl "google.golang.org/protobuf/runtime/protoimpl"
  11. reflect "reflect"
  12. sync "sync"
  13. )
  14. const (
  15. // Verify that this generated code is sufficiently up-to-date.
  16. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
  17. // Verify that runtime/protoimpl is sufficiently up-to-date.
  18. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
  19. )
  20. // This is a compile-time assertion that a sufficiently up-to-date version
  21. // of the legacy proto package is being used.
  22. const _ = proto.ProtoPackageIsVersion4
  23. type User struct {
  24. state protoimpl.MessageState
  25. sizeCache protoimpl.SizeCache
  26. unknownFields protoimpl.UnknownFields
  27. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  28. Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
  29. }
  30. func (x *User) Reset() {
  31. *x = User{}
  32. if protoimpl.UnsafeEnabled {
  33. mi := &file_hello_proto_msgTypes[0]
  34. ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
  35. ms.StoreMessageInfo(mi)
  36. }
  37. }
  38. func (x *User) String() string {
  39. return protoimpl.X.MessageStringOf(x)
  40. }
  41. func (*User) ProtoMessage() {}
  42. func (x *User) ProtoReflect() protoreflect.Message {
  43. mi := &file_hello_proto_msgTypes[0]
  44. if protoimpl.UnsafeEnabled && x != nil {
  45. ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
  46. if ms.LoadMessageInfo() == nil {
  47. ms.StoreMessageInfo(mi)
  48. }
  49. return ms
  50. }
  51. return mi.MessageOf(x)
  52. }
  53. // Deprecated: Use User.ProtoReflect.Descriptor instead.
  54. func (*User) Descriptor() ([]byte, []int) {
  55. return file_hello_proto_rawDescGZIP(), []int{0}
  56. }
  57. func (x *User) GetName() string {
  58. if x != nil {
  59. return x.Name
  60. }
  61. return ""
  62. }
  63. func (x *User) GetAge() int32 {
  64. if x != nil {
  65. return x.Age
  66. }
  67. return 0
  68. }
  69. var File_hello_proto protoreflect.FileDescriptor
  70. var file_hello_proto_rawDesc = []byte{
  71. 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70,
  72. 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2c, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04,
  73. 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
  74. 0x12, 0x10, 0x0a, 0x03, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61,
  75. 0x67, 0x65, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70,
  76. 0x72, 0x6f, 0x74, 0x6f, 0x33,
  77. }
  78. var (
  79. file_hello_proto_rawDescOnce sync.Once
  80. file_hello_proto_rawDescData = file_hello_proto_rawDesc
  81. )
  82. func file_hello_proto_rawDescGZIP() []byte {
  83. file_hello_proto_rawDescOnce.Do(func() {
  84. file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData)
  85. })
  86. return file_hello_proto_rawDescData
  87. }
  88. var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
  89. var file_hello_proto_goTypes = []interface{}{
  90. (*User)(nil), // 0: proto.User
  91. }
  92. var file_hello_proto_depIdxs = []int32{
  93. 0, // [0:0] is the sub-list for method output_type
  94. 0, // [0:0] is the sub-list for method input_type
  95. 0, // [0:0] is the sub-list for extension type_name
  96. 0, // [0:0] is the sub-list for extension extendee
  97. 0, // [0:0] is the sub-list for field type_name
  98. }
  99. func init() { file_hello_proto_init() }
  100. func file_hello_proto_init() {
  101. if File_hello_proto != nil {
  102. return
  103. }
  104. if !protoimpl.UnsafeEnabled {
  105. file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
  106. switch v := v.(*User); i {
  107. case 0:
  108. return &v.state
  109. case 1:
  110. return &v.sizeCache
  111. case 2:
  112. return &v.unknownFields
  113. default:
  114. return nil
  115. }
  116. }
  117. }
  118. type x struct{}
  119. out := protoimpl.TypeBuilder{
  120. File: protoimpl.DescBuilder{
  121. GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
  122. RawDescriptor: file_hello_proto_rawDesc,
  123. NumEnums: 0,
  124. NumMessages: 1,
  125. NumExtensions: 0,
  126. NumServices: 0,
  127. },
  128. GoTypes: file_hello_proto_goTypes,
  129. DependencyIndexes: file_hello_proto_depIdxs,
  130. MessageInfos: file_hello_proto_msgTypes,
  131. }.Build()
  132. File_hello_proto = out.File
  133. file_hello_proto_rawDesc = nil
  134. file_hello_proto_goTypes = nil
  135. file_hello_proto_depIdxs = nil
  136. }

大致看了一眼,乱七八糟的也不知道都是啥意思,但是不要急,看关键的这段就行:

  1. type User struct {
  2. state protoimpl.MessageState
  3. sizeCache protoimpl.SizeCache
  4. unknownFields protoimpl.UnknownFields
  5. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  6. Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
  7. }

我们知道了,golang里面,是以结构体的方式来对接这个message的。message里的每一个字段,都演变成struct里的子元素。这样就可以了。

好了。对于message有1个大致的映像就可以了。我们继续讲message的语法

  1. syntax = "proto3";
  2. message User {
  3. string name=1;
  4. int32 age=2;
  5. }

开头的syntax = "proto3";是来申明这个文件是基于ptoro3语法规则的,有点类似于Python3的文件头部#!/usr/bin/python3申明一样。看了官方文章介绍说,之前也有proto2的版本,protoc解释器默认是proto2的语法。但是,我把proto3改成proto2,执行,却报错了。迷惑不解。

所以,就不纠结是2还是3,直接写3就完事了。但是这一行内容,得写在文件的最开始。

message 后面是消息体的名字, 命名规则是首字母大写,大驼峰的方式。如果不是大写,转换成go语言的时候,会把自动把首首字母变成大写。,转成成php语言,不会大小写转换,其他语言暂不清楚。

  1. #转换go
  2. string name_a=1; ===> NameA string
  3. string name_A=1; ===> Name_A string
  4. string nameA=1; ===> NameA string

花括号里面,就是具体的字段了:

  1. string name=1;
  2. int32 age=2;

大致看一下,是先定义字段的类型,是字符型还是整型,这个好理解,可是名字后面的=1,=2 是什么鬼???是说,name 赋值为1,age 赋值为2 吗?

然而,并不是这样,也并没有我们想的这么简单。查看一下官方文档关于这个数字标识:

消息请求结构体中每个字段都有唯一的数字标识,这些标识用来在消息的二进制格式中识别你的字段,并且一旦消息投入使用,这些标识就不应该再被修改。
标识1-15使用一个字节编码,包括数字和字段类型;标识16-2047使用两个字节编码。所以应该保留1-15,用作出现最频繁的消息类型的标识,不要把1-15用完,为以后留点。

所以,对于新手而已,怎么用呢?我的理解就是随便,只要不重复就好了:

  1. message user1 {
  2. string name_a=1;
  3. int32 age_1=2;
  4. string address=5;
  5. int32 gender=3;
  6. }
  7. message user2 {
  8. string name_a=1;
  9. int32 age_1=2;
  10. string address=50;
  11. int32 gender=4;
  12. }

对于新手而已,不要过多的被这个特性给吓唬住了,等你熟练使用了,再去思考他的原理。

message里面的字段的类型有多种,上面列出了2种,string,int32,同时还有这些:

数字类型: double、float、int32、int64、uint32、uint64、sint32、sint64: 存储长度可变的浮点数、整数、无符号整数和有符号整数
存储固定大小的数字类型:fixed32、fixed64、sfixed32、sfixed64: 存储空间固定
布尔类型: bool
字符串: string
bytes: 字节数组
message: 消息类型
enum :枚举类型

我们可以根据我们实际的情况,来定义它们的字段类型。

下面我们来看其他重要的点。

2. package

我们在定义一个.proto文件时,需要申明这个文件属于哪个包,主要是为了规范整合以及避免重复,这个概念在其他语言中也存在,比如php中的namespace的概念,go中的 package概念。

所以,我们根据实际的分类情况,给每1个proto文件都定义1个包,一般这个包和proto所在的文件夹名子,保持一致。

比如例子中,文件在proto文件夹中,那我们用的package 就为: proto;

3. option

看这个名字,就知道是选项和配置的意思,常见的选项是配置 go_package

  1. option go_package = ".;proto";

现在protoc命令生成go包的时候,如果这一行没加上,会提示错误:

  1. proto git:(master) protoc --go_out=:. hello.proto
  2. 2020/05/21 15:59:40 WARNING: Missing 'go_package' option in "hello.proto", please specify:
  3. option go_package = ".;proto";
  4. A future release of protoc-gen-go will require this be specified.
  5. See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.

所以,这个go_package和上面那个package proto;有啥区别呢?有点蒙啊。

我尝试这样改一下:

  1. syntax = "proto3";
  2. package protoB;
  3. option go_package = ".;protoA";

我看下,生成的go语言包的package到底是啥?打开,生成后的go文件:

  1. # vi hello.pb.go
  2. package protoA
  3. ...

发现是protoA,说明,go的package是受option go_package影响的。所以,在我们没有申请这一句的时候,系统就会用proto文件的package名字,为提示,让你也加上相同的go_package名字了。

再来看一下,这个=".;proto" 是啥意思。我改一下:

  1. option go_package = "./protoA";

执行后,发现,生成了一个protoA文件夹。里面的hello.pb.go文件的package也是protoA。

所以,.;表示的是就在本目录下的意思么???行吧。

再来看一下,我们改成1个绝对的路径目录:

  1. option go_package = "/";

所以,总结一下:

  1. package protoB; //这个用来设定proto文件自身的package
  2. option go_package = ".;protoA"; //这个用来生成的go文件package。一般情况下,可以把这2个设置成一样

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注