RPC 編程 -四-:protobuf 語法學習
- 介紹
Protobuf
是Protocol Buffers
的簡稱,它是谷歌公司開發的一種數據描述語言,並於 2008 年開源。Protobuf
剛開源時的定位類似於XML、JSON
等數據描述語言,通過附帶工具生成代碼並實現將結構化數據序列化的功能。
在序列化結構化數據的機制中,ProtoBuf
是靈活、高效、自動化的,相對常見的XML、JSON
,描述同樣的信息,ProtoBuf
序列化後數據量更小、序列化 / 反序列化速度更快、更簡單。
- XML、JSON、Protobuf 對比
-
json
: 一般的web
項目中,最流行的主要還是json
。因爲瀏覽器對於json
數據支持非常好,有很多內建的函數支持。 -
xml
: 在webservice
中應用最爲廣泛,但是相比於json
,它的數據更加冗餘,因爲需要成對的閉合標籤。json
使用了鍵值對的方式,不僅壓縮了一定的數據空間,同時也具有可讀性。 -
protobuf
: 是後起之秀,是谷歌開源的一種數據格式,適合高性能,對響應速度有要求的數據傳輸場景。因爲profobuf
是二進制數據格式,需要編碼和解碼。數據本身不具有可讀性。因此只能反序列化之後得到真正可讀的數據。
- 定義語法
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 示例說明
-
syntax
: 用來標記當前使用proto
的哪個版本。 -
package
: 包名,用來防止消息類型命名衝突。 -
option go_package
: 選項信息,代表生成後的go
代碼包路徑。 -
message
: 聲明消息的關鍵字,類似Go
語言中的struct
。 -
定義字段語法:
類型 字段名 標識號
標識號說明
每個字段都有唯一的一個數字標識符,一旦開始使用就不能夠再改變。
[1, 15] 之內的標識號在編碼的時候會佔用一個字節。[16, 2047] 之內的標識號則佔用 2 個字節。
最小的標識號可以從 1 開始,最大到 2^29 - 1, or 536,870,911。不可以使用其中的 [19000-19999], 因爲是預留信息,如果使用,編譯時會報警。
3.3 簡單類型與 Go 對應
- 保留標識符 (
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;
}
- 枚舉類型
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 語法,枚舉類的第一個值總是默認值.
- 嵌套消息
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,`
}
- 切片 (數組) 類型消息
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,`
}
...
- 引入其他
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
- 定義服務 (
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!"
- 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
}
- 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