gRPC-Go基础(2)protobuf基础

发布时间:2023年12月28日

0. 简介

前面讲过protobuf工具protoc的使用,本章我们将简单介绍一下protobuf的语法和相关细节。

1. Message

protobuf实际上是一套类似于Json或者XML的数据传输格式和规范,用于不同应用、平台之间的通信,而message就是作为protobuf中定义通信的数据格式。如下,我们可以定义一个message结构:

message Foo {}

在protoc-gen-go插件的作用下,会生成一个名为Foo的结构体,且*Foo会实现proto.Message接口,生成的pg.go文件如下:

type Foo struct {
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields
}

func (x *Foo) Reset() {
   *x = Foo{}
   if protoimpl.UnsafeEnabled {
      mi := &file_simplepb_simple_proto_msgTypes[0]
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      ms.StoreMessageInfo(mi)
   }
}

func (x *Foo) String() string {
   return protoimpl.X.MessageStringOf(x)
}

func (*Foo) ProtoMessage() {}

func (x *Foo) ProtoReflect() protoreflect.Message {
   mi := &file_simplepb_simple_proto_msgTypes[0]
   if protoimpl.UnsafeEnabled && x != nil {
      ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
      if ms.LoadMessageInfo() == nil {
         ms.StoreMessageInfo(mi)
      }
      return ms
   }
   return mi.MessageOf(x)
}

其中,Reset()、String()和ProtoMessage()都是MessageV1定义的接口,在我们的版本中并用不到,为了兼容,生成器还是生成了这部分接口实现。

type MessageV1 interface {
   Reset()
   String() string
   ProtoMessage()
}

而实际中真正用到的是MessageV2,这个接口定义了一个ProtoReflect()方法,此方法返回一个名为protoreflect.Message的接口,这个接口提供基于反射的message接口定义。

2. Message中的字段

上面简单介绍了结构体的生成,接下来介绍一下结构体中的字段。比如人员描述的相关对象如下:

message Profile {
  string name = 1;
  int32 age = 2;
}

2.1 字段编号
message中的每一个字段都有一个唯一的编号,编号的范围从1到229-1,这些编号用于在编码后的二进制中标识字段,对于这些编号,特别是对于与外部平台或者设备交互的接口,一旦确定就不能更改,否则可能造成不同版本之间的不兼容。
需要注意的是,当字段编号在1-15时,在protobuf的编码规则下,其只需一个byte即可编码编号和类型;而当字段编号在16-2047时,就需要两个byte了。所以针对高并发、频繁调用的数据接口,最好使用字段1-15。
另外,我们不能使用数字19000-19999作为字段编号,这是为protobuf保留的。
2.2 字段规则
字段的规则可以有以下几种:

  • singular:对proto3而言,当未设置其他规则时,这就是默认规则;如果是零值时,将不会被序列化,所以对于接收到的默认零值,我们无法判断这是对方设置的还是默认生成的。在go中,这倒并不影响,对于非message字段(message类型也可以作为message中的字段),其他所有singular规则字段都是非指针的,默认就是零值,和proto是对应的,但是proto中的零值和不设置需要表现出一致的含义。
  • optional:和singular相同,只是可以检查该值是否明确;当该字段被设置时,它可以被正常的序列化和反序列化;当未设置时,会返回默认值,则不会被序列化;对于go而言,optional规则的字段会被指针化,其和proto定义是一致的,不设置和设置为零值可以表现出两个含义。
  • repeated:重复值,映射到go中就是切片。
  • map:键值对类型,映射到go中就是map。

如下,我们设置以下的字段规则;其生成的pb.go中结构体也不尽相同。
处。

message Profile {
  string name = 1;
  optional int32 age = 2;
  repeated string hobbies = 3;
  map<int32,int32> calender = 4;
}

生成的pb.go文件如下:

type Profile struct {
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields

   Name     string          `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
   Age      *int32          `protobuf:"varint,2,opt,name=age,proto3,oneof" json:"age,omitempty"`
   Hobbies  []string        `protobuf:"bytes,3,rep,name=hobbies,proto3" json:"hobbies,omitempty"`
   Calender map[int32]int32 `protobuf:"bytes,4,rep,name=calender,proto3" json:"calender,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}

2.3 保留字段
如果我们更改了接口,删除了一些不再使用的字段,那么后来者可能会使用这些字段表示另外一些含义,对于一些对外的接口,可能引起版本之间的不兼容,可能会引起数据损坏、隐私暴露等严重错误,所以我们使用reserve字段来保留这些字段,防止错误的发生,如下:

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

2.4 字段类型
字段的类型众多,和各大语言之间的对应关系如:Scalar Value Types
2.5 字段默认值
对于编码后的数据而言,如果singular字段没有被设置值,那么相应字段会被设置为该字段的默认值。不同类型的字段有着不同的默认值:

  • string:默认零值是空字符串,go中是"";
  • bytes:默认是空字节数组,go中是[]byte(nil);
  • bool:默认是否,go中是false;
  • numeric:数值类型,都是0;
  • enum:枚举值,其默认值一定是其定义的第一个枚举量,必然为0;
  • message:对于以message作为字段类型的,对于不同的语言其表现是不同的,在go语言中,其被映射为结构体指针,所以其零值是结构体的nil。

需要注意的是,当接收方拿到零值类型对象的时候,无法判断这个对象是由对方设置了零值,还是没有设置任何值,所以,不要用零值去触发一些特定的行为,可能会发生不可预计的事情。在go中,如果需要区分设置了零值和没有设置任何值,可以用optional规则。
另外,零值是不会被序列化的。

3. 枚举类型

message Profile {
  string name = 1;
  optional int32 age = 2;
  repeated string hobbies = 3;
  map<int32,int32> calender = 4;

  enum Gender {
    UNKNOWN = 0;
    MALE = 1;
    FEMALE = 2;
  }
  Gender gender = 5;
}

如上,gender字段就是枚举类型,其有以下两个特点:

  • 枚举类型必须有一个零值枚举,且这个零值枚举是枚举值的默认值;
  • 零值必须是第一个枚举,以兼容proto2中第一个枚举值肯定是默认值的语义。

可以使用option allow_alias = true使用枚举别名,如下,这时候,MAN和MALE就互为别名。

enum Gender {
  option allow_alias = true;
  UNKNOWN = 0;
  MAN = 1;
  MALE = 1;
  WOMAN = 2;
  FEMALE = 2;
}

值得注意的是,枚举常量是32位的int常量,因为其使用变长编码,所以使用负数作为枚举值的序列化效率是很低的。

4. Map类型

protobuf的map类型定义如下,其中,key_type可以是字符串或者整型,value_type可以是除了另一个map外的所有类型。

map<key_type, value_type> map_field = N;

5. Oneof类型

如果对于消息中,有多个字段,同时至多只能选择一个字段的,可以使用oneof类型,oneof类型的字段中所有字段共享内存,如果一个字段被设置,那么其他字段自动被删除。如果设置了多个值,那么最后一个设置的值将覆盖前面的值。
如下,我们可以设置Profile的头像Avatar如下,其头像可以是图片存储的url,也可以直接是图片数据,这里我们就可以使用oneof类型。

package account;

message Profile {
  string name = 1;
  int32 age = 2;
  repeated string hobbies = 3;
  map<int32,int32> calender = 4;

  enum Gender {
    option allow_alias = true;
    UNKNOWN = 0;
    MAN = 1;
    MALE = 1;
    WOMAN = 2;
    FEMALE = 2;
  }
  Gender gender = 5;

  oneof avatar {
    string image_url = 6;
    bytes image_data = 7;
  }
}

设置值的时候可以这样写:

p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{"http://example.com/image.png"},
}

// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{imageData},
}

解析字段的时候可以这样写:

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
        // Load profile image based on URL
        // using x.ImageUrl
case *account.Profile_ImageData:
        // Load profile image based on bytes
        // using x.ImageData
case nil:
        // The field is not set.
default:
        return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}

6. Any类型

Any类型允许我们使用此字段传递任何一个protobuf类型的消息,类似于某些语言中泛型,如下,其只有两个字段,value字段是序列化后的二进制值,type_url则是该类型的唯一标识符,在golang中,所有message的唯一标识符都可以通过反射m.ProtoReflect().Descriptor().FullName()拿到。
为了使用any类型,我们需要import “google/protobuf/any.proto”。proto文件的下载地址为链接。

any

message Any {
  // ...
  string type_url = 1;

  // Must be a valid serialized protocol buffer of the above specified type.
  bytes value = 2;
}

golang库的底层提供了一些函数用于发送或者接收的函数,譬如,发送时,我们可以使用以下方式序列化我们希望序列化的消息:

detail, err := anypb.New(&AnyA{})

解析接收数据时,我们可以用以下方法判断类型或者反序列化消息:

// MessageIs 判断是否为此message类型
m.Detail.MessageIs((*AnyA)(nil))

// UnmarshalTo 将any解析到具体类型
var a AnyA
err := m.Detail.UnmarshalTo(&a)

// UnmarshalNew 将消息类型转回动态类型
dy, err := m.Detail.UnmarshalNew()
_, ok := dy.(*AnyA)

7. Struct类型

struct类型和any类型是类似的,都是protobuf提供的不透明配置嵌入的方式。不同的是,any是将带有类型信息的二进制序列化的protobuf嵌入到另一个protobuf的字段内;而struct是最外层不能是数组的动态json类型。

message Struct {
  // Unordered map of dynamically typed values.
  map<string, Value> fields = 1;
}

// JSON 数据类型
message Value {
  // The kind of value.
  oneof kind {
    // Represents a null value.
    NullValue null_value = 1;
    // Represents a double value.
    double number_value = 2;
    // Represents a string value.
    string string_value = 3;
    // Represents a boolean value.
    bool bool_value = 4;
    // Represents a structured value.
    Struct struct_value = 5;
    // Represents a repeated `Value`.
    ListValue list_value = 6;
  }
}

// JSON null
enum NullValue {
  // Null value.
  NULL_VALUE = 0;
}

// JSON 数组
message ListValue {
  repeated Value values = 1;
}

同样的,为了使用struct类型,我们需要import “google/protobuf/struct.proto”。proto文件的下载地址为链接。

我们也可以使用以下的方法去创建struct类型:

// NewStruct 从map表创建struct
s, err := structpb.NewStruct(m)

// UnmarshalJSON 从json串生成strcut
s := &_struct.Struct{}
marshal, _ := json.Marshal(data)
err := s.UnmarshalJSON(marshal)

可以使用以下的方法去解析接收到的strcut类型数据:

// AsMap 将strcut转换成map
m := s.AsMap()

// MarshalJSON 将struct转换为[]byte
data, err := s.MarshalJSON()
_ = json.Unmarshal(data, v)

文章来源:https://blog.csdn.net/ldxxxxll/article/details/135242709
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。