1. gRPC簡介
gRPC 是一個高性能、開源、通用的RPC框架,由Google推出,基於HTTP2協議標准設計開發,默認采用Protocol Buffers數據序列化協議,支持多種開發語言。gRPC提供了一種簡單的方法來精確的定義服務,並且為客戶端和服務端自動生成可靠的功能庫。gRPC 是一個高性能、開源、通用的RPC框架,由Google推出,基於HTTP2協議標准設計開發,默認采用Protocol Buffers數據序列化協議,支持多種開發語言。gRPC提供了一種簡單的方法來精確的定義服務,並且為客戶端和服務端自動生成可靠的功能庫。
在gRPC客戶端可以直接調用不同服務器上的遠程程序,使用姿勢看起來就像調用本地程序一樣,很容易去構建分布式應用和服務。和很多RPC系統一樣,服務端負責實現定義好的接口並處理客戶端的請求,客戶端根據接口描述直接調用需要的服務。客戶端和服務端可以分別使用gRPC支持的不同語言實現。
1.1. 主要特性
強大的IDL:
gRPC使用ProtoBuf來定義服務,ProtoBuf是由Google開發的一種數據序列化協議(類似於XML、JSON、hessian)。ProtoBuf能夠將數據進行序列化,並廣泛應用在數據存儲、通信協議等方面。
多語言支持:
gRPC支持多種語言,並能夠基於語言自動生成客戶端和服務端功能庫。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它語言的版本正在積極開發中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等語言,grpc-java已經支持Android開發。
HTTP2:
gRPC基於HTTP2標准設計,所以相對於其他RPC框架,gRPC帶來了更多強大功能,如雙向流、頭部壓縮、多復用請求等。這些功能給移動設備帶來重大益處,如節省帶寬、降低TCP鏈接次數、節省CPU使用和延長電池壽命等。同時,gRPC還能夠提高了雲端服務和Web應用的性能。gRPC既能夠在客戶端應用,也能夠在服務器端應用,從而以透明的方式實現客戶端和服務器端的通信和簡化通信系統的構建。
更多介紹請查看官方網站
2. Protobuf⇢Go轉換
這里使用一個測試文件對照說明常用結構的protobuf到golang的轉換。只說明關鍵部分代碼,詳細內容請查看完整文件。示例文件在proto/test目錄下。
2.1.1. Package
在proto文件中使用package關鍵字聲明包名,默認轉換成go中的包名與此一致,如果需要指定不一樣的包名,可以使用go_package選項:
package test; option go_package="test";
2.1.2. Message
proto中的message對應go中的struct,全部使用駝峰命名規則。嵌套定義的message,enum轉換為go之后,名稱變為Parent_Child結構。
示例proto:
// Test 測試 message Test { int32 age = 1; int64 count = 2; double money = 3; float score = 4; string name = 5; bool fat = 6; bytes char = 7; // Status 枚舉狀態 enum Status { OK = 0; FAIL = 1; } Status status = 8; // Child 子結構 message Child { string sex = 1; } Child child = 9; map<string, string> dict = 10; }
轉換結果:
// Status 枚舉狀態 type Test_Status int32 const ( Test_OK Test_Status = 0 Test_FAIL Test_Status = 1 ) // Test 測試 type Test struct { Age int32 `protobuf:"varint,1,opt,name=age" json:"age,omitempty"` Count int64 `protobuf:"varint,2,opt,name=count" json:"count,omitempty"` Money float64 `protobuf:"fixed64,3,opt,name=money" json:"money,omitempty"` Score float32 `protobuf:"fixed32,4,opt,name=score" json:"score,omitempty"` Name string `protobuf:"bytes,5,opt,name=name" json:"name,omitempty"` Fat bool `protobuf:"varint,6,opt,name=fat" json:"fat,omitempty"` Char []byte `protobuf:"bytes,7,opt,name=char,proto3" json:"char,omitempty"` Status Test_Status `protobuf:"varint,8,opt,name=status,enum=test.Test_Status" json:"status,omitempty"` Child *Test_Child `protobuf:"bytes,9,opt,name=child" json:"child,omitempty"` Dict map[string]string `protobuf:"bytes,10,rep,name=dict" json:"dict,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` } // Child 子結構 type Test_Child struct { Sex string `protobuf:"bytes,1,opt,name=sex" json:"sex,omitempty"` }
除了會生成對應的結構外,還會有些工具方法,如字段的getter:
func (m *Test) GetAge() int32 { if m != nil { return m.Age } return 0 }
枚舉類型會生成對應名稱的常量,同時會有兩個map方便使用:
var Test_Status_name = map[int32]string{ 0: "OK", 1: "FAIL", } var Test_Status_value = map[string]int32{ "OK": 0, "FAIL": 1, }
2.1.3. Service
定義一個簡單的Service,TestService有一個方法Test,接收一個Request參數,返回Response:
// TestService 測試服務 service TestService { // Test 測試方法 rpc Test(Request) returns (Response) {}; } // Request 請求結構 message Request { string name = 1; } // Response 響應結構 message Response { string message = 1; }
轉換結果:
// 客戶端接口 type TestServiceClient interface { // Test 測試方法 Test(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) } // 服務端接口 type TestServiceServer interface { // Test 測試方法 Test(context.Context, *Request) (*Response, error) }
生成的go代碼中包含該Service定義的接口,客戶端接口已經自動實現了,直接供客戶端使用者調用,服務端接口需要由服務提供方實現。
3. Protobuf語法
3.1.1. 基本規范
1.文件以.proto做為文件后綴,除結構定義外的語句以分號結尾
2.結構定義可以包含:message、service、enum
3.rpc方法定義結尾的分號可有可無
4。Message命名采用駝峰命名方式,字段命名采用小寫字母加下划線分隔方式
message SongServerRequest { required string song_name = 1; }
5.Enums類型名采用駝峰命名方式,字段命名采用大寫字母加下划線分隔方式
enum Foo { FIRST_VALUE = 1; SECOND_VALUE = 2; }
6.Service與rpc方法名統一采用駝峰式命名
3.1.2. 字段規則
1.字段格式:限定修飾符 | 數據類型 | 字段名稱 | = | 字段編碼值 | [字段默認值]
2.限定修飾符包含 required\optional\repeated
Required:表示是一個必須字段,必須相對於發送方,在發送消息之前必須設置該字段的值,對於接收方,必須能夠識別該字段的意思。發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄
Optional:表示是一個可選字段,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該字段的值。對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理。---因為optional字段的特性,很多接口在升級版本中都把后來添加的字段都統一的設置為optional字段,這樣老的版本無需升級程序也可以正常的與新的軟件進行通信,只不過新的字段無法識別而已,因為並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡
Repeated:表示該字段可以包含0~N個元素。其特性和optional一樣,但是每一次可以包含多個值。可以看作是在傳遞一個數組的值
3.數據類型
Protobuf定義了一套基本數據類型。幾乎都可以映射到C++\Java等語言的基礎數據類型
+ N 表示打包的字節並不是固定。而是根據數據的大小或者長度
+ 關於 fixed32 和int32的區別。fixed32的打包效率比int32的效率高,但是使用的空間一般比int32多。因此一個屬於時間效率高,一個屬於空間效率高
4.字段名稱
a. 字段名稱的命名與C、C++、Java等語言的變量命名方式幾乎是相同的
b. protobuf建議字段的命名采用以下划線分割的駝峰式。例如 first_name 而不是firstName
5.字段編碼值
a.有了該值,通信雙方才能互相識別對方的字段,相同的編碼值,其限定修飾符和數據類型必須相同,編碼值的取值范圍為 1~2^32(4294967296)
b.其中 1~15的編碼時間和空間效率都是最高的,編碼值越大,其編碼的時間和空間效率就越低,所以建議把經常要傳遞的值把其字段編碼設置為1-15之間的值
c.1900~2000編碼值為Google protobuf 系統內部保留值,建議不要在自己的項目中使用
6.字段默認值
當在傳遞數據時,對於required數據類型,如果用戶沒有設置值,則使用默認值傳遞到對端
3.1.3. service如何定義
如果想要將消息類型用在RPC系統中,可以在.proto文件中定義一個RPC服務接口,protocol buffer編譯器會根據所選擇的不同語言生成服務接口代碼
例如,想要定義一個RPC服務並具有一個方法,該方法接收SearchRequest並返回一個SearchResponse,此時可以在.proto文件中進行如下定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}
生成的接口代碼作為客戶端與服務端的約定,服務端必須實現定義的所有接口方法,客戶端直接調用同名方法向服務端發起請求,比較麻煩的是,即便業務上不需要參數也必須指定一個請求消息,一般會定義一個空message
3.1.4. Message如何定義
1.一個message類型定義描述了一個請求或響應的消息格式,可以包含多種類型字段
2.例如定義一個搜索請求的消息格式,每個請求包含查詢字符串、頁碼、每頁數目
3.字段名用小寫,轉為go文件后自動變為大寫,message就相當於結構體
syntax = "proto3"; message SearchRequest { string query = 1; // 查詢字符串 int32 page_number = 2; // 頁碼 int32 result_per_page = 3; // 每頁條數 }
4.首行聲明使用的protobuf版本為proto3
5.SearchRequest 定義了三個字段,每個字段聲明以分號結尾,.proto文件支持雙斜線 // 添加單行注釋
3.1.5. 添加更多Message類型
一個.proto文件中可以定義多個消息類型,一般用於同時定義多個相關的消息,例如在同一個.proto文件中同時定義搜索請求和響應消息
syntax = "proto3"; // SearchRequest 搜索請求 message SearchRequest { string query = 1; // 查詢字符串 int32 page_number = 2; // 頁碼 int32 result_per_page = 3; // 每頁條數 } // SearchResponse 搜索響應 message SearchResponse { ... }
3.1.6. 如何使用其他Message
message支持嵌套使用,作為另一message中的字段類型
message SearchResponse { repeated Result results = 1; } message Result { string url = 1; string title = 2; repeated string snippets = 3; }
3.1.7. Message嵌套的使用
1.支持嵌套消息,消息可以包含另一個消息作為其字段。也可以在消息內定義一個新的消息
2.內部聲明的message類型名稱只可在內部直接使用
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; }
3.另外,還可以多層嵌套
message Outer { // Level 0 message MiddleAA { // Level 1 message Inner { // Level 2 int64 ival = 1; bool booly = 2; } } message MiddleBB { // Level 1 message Inner { // Level 2 int32 ival = 1; bool booly = 2; } } }
3.1.8. proto3的Map類型
1.proto3支持map類型聲明
map<key_type, value_type> map_field = N; message Project {...} map<string, Project> projects = 1;
2.鍵、值類型可以是內置的類型,也可以是自定義message類型
3.字段不支持repeated屬性
3.1.9. .proto文件編譯
1.通過定義好的.proto文件生成Java, Python, C++, Go, Ruby, JavaNano, Objective-C, or C# 代碼,需要安裝編譯器protoc
2.當使用protocol buffer編譯器運行.proto文件時,編譯器將生成所選語言的代碼,用於使用在.proto文件中定義的消息類型、服務接口約定等。不同語言生成的代碼格式不同:
a. C++: 每個.proto文件生成一個.h文件和一個.cc文件,每個消息類型對應一個類
b. Java: 生成一個.java文件,同樣每個消息對應一個類,同時還有一個特殊的Builder類用於創建消息接口
c. Python: 姿勢不太一樣,每個.proto文件中的消息類型生成一個含有靜態描述符的模塊,該模塊與一個元類metaclass在運行時創建需要的Python數據訪問類
d. Go: 生成一個.pb.go文件,每個消息類型對應一個結構體
e. Ruby: 生成一個.rb文件的Ruby模塊,包含所有消息類型
f. JavaNano: 類似Java,但不包含Builder類
g. Objective-C: 每個.proto文件生成一個pbobjc.h和一個pbobjc.m文件
h. C#: 生成.cs文件包含,每個消息類型對應一個類
3.1.10. import導入定義
1.可以使用import語句導入使用其它描述文件中聲明的類型
2.protobuf 接口文件可以像C語言的h文件一個,分離為多個,在需要的時候通過 import導入需要對文件。其行為和C語言的#include或者java的import的行為大致相同,例如import "others.proto";
3.protocol buffer編譯器會在 -I / --proto_path參數指定的目錄中查找導入的文件,如果沒有指定該參數,默認在當前目錄中查找
3.1.11. 包的使用
1.在.proto文件中使用package聲明包名,避免命名沖突
syntax = "proto3"; package foo.bar; message Open {...}
2.在其他的消息格式定義中可以使用包名+消息名的方式來使用類型,如
message Foo { ... foo.bar.Open open = 1; ... }
3.在不同的語言中,包名定義對編譯后生成的代碼的影響不同
a. C++ 中:對應C++命名空間,例如Open會在命名空間foo::bar中
b. Java 中:package會作為Java包名,除非指定了option jave_package選項
c. Python 中:package被忽略
d. Go 中:默認使用package名作為包名,除非指定了option go_package選項
e. JavaNano 中:同Java
f. C# 中:package會轉換為駝峰式命名空間,如Foo.Bar,除非指定了option csharp_namespace選項
4. 小案例
按照慣例,這里也從一個Hello項目開始,本項目定義了一個商品 Service。
流程:
- 編寫
.proto
描述文件 - 編譯生成
.pb.go
文件 - 服務端實現約定的接口並提供服務
- 客戶端按照約定調用
.pb.go
文件中的方法請求服務
項目結構:
Step1:編寫描述文件:Prod.proto
syntax="proto3"; package services; message ProdRequest { int32 prod_id = 1; //商品ID } message ProdResponse { int32 prod_stock = 1; //商品庫存 } service ProductService { rpc GetProductStock (ProdRequest) returns (ProdResponse); }
Prod.proto文件中定義了一個ProductService ,該服務包含一個GetProductStock 方法,同時聲明了ProdRequest和ProdResponse消息結構用於請求和響應。客戶端使用ProdRequest參數調用GetProductStock 方法請求服務端,服務端響應ProdResponse消息。一個最簡單的服務就定義好了。
Step2:編譯生成.pb.go
文件
$ cd server/pb
# 編譯Prod.proto
$ protoc -I . --go_out=plugins=grpc:. ./Prod.proto
在當前目錄內生成的Prod.pb.go文件,按照.proto文件中的說明,包含服務端接口ProductServer描述,客戶端接口及實現ProdClient,及ProdRequest、ProdResponse結構體。
注意:不要手動編輯該文件
server/services/ProductService.go
package services import context "context" type ProdService struct{} func (*ProdService) GetProductStock(ctx context.Context, req *ProdRequest) (*ProdResponse, error) { return &ProdResponse{ ProdStock: 100, }, nil }
Step3:實現服務端接口 server/server.go
package main import ( "go_code/grpc-1/server/services" "google.golang.org/grpc" "log" "net" ) func main() { rpcServer := grpc.NewServer() services.RegisterProductServiceServer(rpcServer, new(services.ProdService)) listen, err := net.Listen("tcp", ":8081") if err != nil { log.Fatalf("啟動網絡監聽失敗 %v\n", err) } rpcServer.Serve(listen) }
服務端引入編譯后的proto包,定義一個空結構用於實現約定的接口,接口描述可以查看hello.pb.go文件中的HelloServer接口描述。實例化grpc Server並注冊HelloService,開始提供服務。
運行:
go run server.go
Step4:實現客戶端調用 client/client.go
package main import ( "context" "fmt" "log" "go_code/grpc-1/client/services" "google.golang.org/grpc" ) func main() { conn, err := grpc.Dial(":8081", grpc.WithInsecure()) if err != nil { log.Fatalf("連接GRPC服務端失敗 %v\n", err) } defer conn.Close() prodClient := services.NewProductServiceClient(conn) prodRes, err := prodClient.GetProductStock(context.Background(), &services.ProdRequest{ProdId: 12}) if err != nil { log.Fatalf("請求GRPC服務端失敗 %v\n", err) } fmt.Println(prodRes.ProdStock) }
客戶端初始化連接后直接調用hello.pb.go中實現的SayHello方法,即可向服務端發起請求,使用姿勢就像調用本地方法一樣。
運行:
go run client.go 100 // 接收到服務端響應
如果你收到了"100"的回復,恭喜你已經會使用grpc了。
建議到這里仔細看一看Prod.pb.go文件中的內容,對比Prod.proto文件,理解protobuf中的定義轉換為golang后的結構。