RPC 編程 -四-:protobuf 語法學習

  1. 介紹

ProtobufProtocol Buffers的簡稱,它是谷歌公司開發的一種數據描述語言,並於 2008 年開源。Protobuf剛開源時的定位類似於XML、JSON等數據描述語言,通過附帶工具生成代碼並實現將結構化數據序列化的功能。

在序列化結構化數據的機制中,ProtoBuf是靈活、高效、自動化的,相對常見的XML、JSON,描述同樣的信息,ProtoBuf序列化後數據量更小、序列化 / 反序列化速度更快、更簡單。

  1. XML、JSON、Protobuf 對比

nKKvfi

  1. 定義語法

3.1 示例

// 指明當前使用proto3語法,如果不指定,編譯器會使用proto2
syntax = "proto3";
// package聲明符,用來防止消息類型有命名衝突
package msg;
// 選項信息,對應go的包路徑
option go_package = "server/msg";
// message關鍵字,像go中的結構體
message FirstMsg {
  // 類型 字段名 標識號
  int32 id = 1;
  string name=2;
  string age=3;
}

3.2 示例說明

標識號說明

  • 每個字段都有唯一的一個數字標識符,一旦開始使用就不能夠再改變。

  • [1, 15] 之內的標識號在編碼的時候會佔用一個字節。[16, 2047] 之內的標識號則佔用 2 個字節。

  • 最小的標識號可以從 1 開始,最大到 2^29 - 1, or 536,870,911。不可以使用其中的 [19000-19999], 因爲是預留信息,如果使用,編譯時會報警。

3.3 簡單類型與 Go 對應

eH35Qd

  1. 保留標識符 (reserved)

什麼是保留標示符?reserved 標記的標識號、字段名,都不能在當前消息中使用。

syntax = "proto3";
package demo;

// 在這個消息中標記
message DemoMsg {
  // 標示號:1,2,10,11,12,13 都不能用
  reserved 1,2, 10 to 13;
  // 字段名 test、name 不能用
  reserved "test","name";
  // 不能使用字段名,提示:Field name 'name' is reserved
  string name = 3;
  // 不能使用標示號,提示:Field 'id' uses reserved number 11
  int32 id = 11;
}

// 另外一個消息還是可以正常使用
message Demo2Msg {
  // 標示號可以正常使用
  int32 id = 1;
  // 字段名可以正常使用
  string name = 2;
}
  1. 枚舉類型

syntax = "proto3";
package demo;
// 聲明生成Go代碼,包路徑
option go_package ="server/demo";
// 枚舉消息
message DemoEnumMsg {
  enum Gender{
    // 枚舉字段標識符,必須從0開始
    UnKnown = 0;
    Body = 1;
    Girl = 2;
  }
  // 使用自定義的枚舉類型
  Gender sex = 2;
}
// 在枚舉信息中,重複使用標識符
message DemoTwoMsg{
  enum Animal {
    // 開啓允許重複使用 標示符
    option allow_alias = true;
    Other = 0;
    Cat = 1;
    Dog = 2;
    // 白貓也是毛,標示符也用1
    // 不開啓allow_alias,會報錯:Enum value number 1 has already been used by value 'Cat'
    WhiteCat = 1;
  }
}

每個枚舉類型必須將其第一個類型映射爲 0, 原因有兩個:1. 必須有個默認值爲 0;2. 爲了兼容 proto2 語法,枚舉類的第一個值總是默認值.

  1. 嵌套消息

syntax = "proto3";
option go_package = "server/nested";
// 學員信息
message UserInfo {
  int32 userId = 1;
  string userName = 2;
}
message Common {
  // 班級信息
  message CLassInfo{
    int32 classId = 1;
    string className = 2;
  }
}
// 嵌套信息
message NestedDemoMsg {
  // 學員信息 (直接使用消息類型)
  UserInfo userInfo = 1;
  // 班級信息 (通過Parent.Type,調某個消息類型的子類型)
  Common.CLassInfo classInfo =2;
}

7.map 類型消息

7.1 protobuf源碼

syntax = "proto3";
option go_package = "server/demo";

// map消息
message DemoMapMsg {
  int32 userId = 1;
  map<string,string> like =2;
}

7.2 生成Go代碼

// map消息
type DemoMapMsg struct {
 state         protoimpl.MessageState
 sizeCache     protoimpl.SizeCache
 unknownFields protoimpl.UnknownFields

 UserId int32             `protobuf:"varint,1,opt,`
 Like   map[string]string `protobuf:"bytes,2,rep,`
}
  1. 切片 (數組) 類型消息

8.1 protobuf源碼

syntax = "proto3";
option go_package = "server/demo";

// repeated允許字段重複,對於Go語言來說,它會編譯成數組(slice of type)類型的格式
message DemoSliceMsg {
  // 會生成 []int32
  repeated int32 id = 1;
  // 會生成 []string
  repeated string name = 2;
  // 會生成 []float32
  repeated float price = 3;
  // 會生成 []float64
  repeated double money = 4;
}

8.2 生成Go代碼

...
// repeated允許字段重複,對於Go語言來說,它會編譯成數組(slice of type)類型的格式
type DemoSliceMsg struct {
 state         protoimpl.MessageState
 sizeCache     protoimpl.SizeCache
 unknownFields protoimpl.UnknownFields

 // 會生成 []int32
 Id []int32 `protobuf:"varint,1,rep,packed,`
 // 會生成 []string
 Name []string `protobuf:"bytes,2,rep,`
 // 會生成 []float32
 Price []float32 `protobuf:"fixed32,3,rep,packed,`
 Money []float64 `protobuf:"fixed64,4,rep,packed,`
}
...
  1. 引入其他proto文件

9.1 被引入文件class.proto

文件位置:proto/class.proto

syntax="proto3";
// 包名
package dto;
// 生成go後的文件路徑
option go_package = "grpc/server/dto";

message ClassMsg {
  int32  classId = 1;
  string className = 2;
}

9.2 使用引入文件user.proto

文件位置:proto/user.proto

syntax = "proto3";

// 導入其他proto文件
import "proto/class.proto";

option go_package="grpc/server/dto";

package dto;

// 用戶信息
message UserDetail{
  int32 id = 1;
  string name = 2;
  string address = 3;
  repeated string likes = 4;
  // 所屬班級
  ClassMsg classInfo = 5;
}

9.3 Idea 提示: Cannot resolve import..

img

  1. 定義服務 (Service)

上面學習的都是怎麼定義一個消息類型,如果想要將消息類型用在 RPC(遠程方法調用) 系統中,需要使用關鍵字 (service) 定義一個 RPC 服務接口,使用rpc定義具體方法,而消息類型則充當方法的參數和返回值。

10.1 編寫service

文件位置:proto/hello_service.proto

syntax = "proto3";

option go_package = "grpc/server";

// 定義入參消息
message HelloParam{
  string name = 1;
  string context = 2;
}

// 定義出參消息
message HelloResult {
  string result = 1;
}

// 定義service
service HelloService{
  // 定義方法 
  rpc SayHello(HelloParam) returns (HelloResult);
}

10.2 生成Go代碼

使用命令:protoc --go_out=. --go-grpc_out=. proto/hello_service.proto生成代碼。

上述命令會生成很多代碼, 我們的工作主要是要實現SayHello方法, 下面是生成的部分代碼。

1. 客戶端部分代碼

// 客戶端接口
type HelloServiceClient interface {
 SayHello(ctx context.Context, in *HelloParam, opts ...grpc.CallOption) (*HelloResult, error)
}
// 客戶端實現調用SayHello方法
func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloParam, opts ...grpc.CallOption) (*HelloResult, error) {
 out := new(HelloResult)
 err := c.cc.Invoke(ctx, "/HelloService/SayHello", in, out, opts...)
 if err != nil {
  return nil, err
 }
 return out, nil
}

2. 服務端部分代碼

// 生成的接口
type HelloServiceServer interface {
 SayHello(context.Context, *HelloParam) (*HelloResult, error)
 mustEmbedUnimplementedHelloServiceServer()
}
// 需要實現SayHello方法
func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloParam) (*HelloResult, error) {
 return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}

10.3 實現服務端 (SayHello) 方法

func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) {
 //return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
 return &HelloResult{Result: fmt.Sprintf("%s say %s",p.GetName(),p.GetContext())},nil
}

10.4 運行服務

1. 編寫服務端

package main

import (
 "52lu/go-rpc/grpc/server"
 "fmt"
 "google.golang.org/grpc"
 "net"
)
// 服務端代碼
func main() {
 // 創建grpc服務
 rpcServer := grpc.NewServer()
 // 註冊服務
 server.RegisterHelloServiceServer(rpcServer, new(server.UnimplementedHelloServiceServer))
 // 監聽端口
 listen, err := net.Listen("tcp"":1234")
 if err != nil {
  fmt.Println("服務啓動失敗", err)
  return
 }
 fmt.Println("服務啓動成功!")
 rpcServer.Serve(listen)
}

2. 編寫客戶端

package main

import (
 "52lu/go-rpc/grpc/server"
 "context"
 "fmt"
 "google.golang.org/grpc"
)

// 客戶端代碼
func main() {
 // 建立鏈接
 dial, err := grpc.Dial("127.0.0.1:1234", grpc.WithInsecure())
 if err != nil {
  fmt.Println("Dial Error ", err)
  return
 }
 // 延遲關閉鏈接
 defer dial.Close()
 // 實例化客戶端
 client := server.NewHelloServiceClient(dial)
 // 發起請求
 result, err := client.SayHello(context.TODO()&server.HelloParam{
  Name:    "張三",
  Context: "hello word!",
 })
 if err != nil {
  fmt.Println("請求失敗:", err)
  return
 }
 // 打印返回信息
 fmt.Printf("%+v\n", result)
}

3. 啓動運行

# 啓動服務端
➜ go run server.go
服務啓動成功!

# 啓動客戶端
➜  go run client.go
result:"張三 say hello word!"
  1. oneof(只能選擇一個)

11.1 編寫proto

修改上面的服務中的請求參數:HelloParam,

// 定義入參消息
message HelloParam{
  string name = 1;
  string context = 2;
  // oneof 最多隻能設置其中一個字段
  oneof option {
    int32 age= 3;
    string gender= 4;
  }
}

11.2 使用

1. 客戶端傳參

生成Go代碼後,入參只能設置其中一個值,如下

...省略
 // 實例化客戶端
 client := server.NewHelloServiceClient(dial)
 // 定義參數
 reqParam := &server.HelloParam{
  Name:    "張三",
  Context: "hello word!",
 }
 // 只能設置其中一個
 reqParam.Option = &server.HelloParam_Age{Age: 19}
 // 這個會替代上一個值
 //reqParam.Option = &server.HelloParam_Gender{Gender: "男"}
 // 發起請求
 result, err := client.SayHello(context.TODO(), reqParam)
...

2. 服務端接收參數

func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) {
 //return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
 res := fmt.Sprintf("name:%s| gender:%s| age:%s | context:%s",p.GetName(),p.GetGender(),p.GetAge(),p.GetContext())
 return &HelloResult{Result:res,},nil
}
  1. Any

12.1  編寫proto

syntax = "proto3";

option go_package = "grpc/server";

// 使用any類型,需要導入這個
import "google/protobuf/any.proto";

// 定義準備傳的消息
message Context {
  int32 id = 1;
  string title = 2;
}
// 定義入參消息
message HelloParam{
  // any,代表可以是任何類型
  google.protobuf.Any data = 1;
}

// 定義出參消息
message HelloResult {
  string result = 1;
}

12.2 使用

1. 客戶端傳參

//...省略...
 // 使用Any參數
 content := &server.Context{
  Id:    100,
  Title: "Hello Word",
 }
 // 序列化Any類型參數
 any, err := anypb.New(content)
 if err != nil {
  fmt.Println("any 類型參數序列化失敗")
  return
 }
  // 注意這裏,一開始在proto文件中,沒有定義使用消息類型Context,
  // 現在通過any類型,同樣可以使用
 reqParam := &server.HelloParam{Data: any}
 // 發起請求
 result, err := client.SayHello(context.TODO(), reqParam)
 if err != nil {
  fmt.Println("請求失敗:", err)
  return
 }
// ....省略...

2. 服務端接收參數

func (UnimplementedHelloServiceServer) SayHello(ctx context.Context, p *HelloParam) (*HelloResult, error) {
 var context Context
  // 反序列化參數
 p.Data.UnmarshalTo(&context)
 res := fmt.Sprintf("%v|%v",context.GetId(),context.GetTitle())
 return &HelloResult{Result: res},nil
 //return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MJyG_1NiSCvPmAvL1mS6Eg