gRPC & Protocol Buffer 構建高性能介面實踐
介紹如何使用 gRPC 和 ProtoBuf,快速瞭解 gRPC 可以參考這篇文章第一段:gRPC quick Start。
介面開發是軟體開發佔據舉足輕重的地位,是現代軟體開發之基石。體現在無論是前後端分離的 Web 前端還是移動客戶端,乃至基於不同系統、編程語言構建的軟體系統之間,API 都是充當橋樑的作用把不同端的系統鏈接在一起從而形成了一套穩固的商用系統。
基於 Web 的介面通常都是 RESTful API 結合 JSON 在前後端之間傳遞信息,這種模式比較適合於前後端分離及移動客戶端於後端通信;但對於承載大規模並發、高性能要求的微服務架構,基於 JSON 傳輸的 RESTful 是否還適用於高並發、伸縮性強及業務邏輯複雜的軟體架構嗎?基於 RESTful 架構是否能夠簡單是想雙向流 (bidrectional stream) 的介面。gRPC 和 protocol buffer 就是解決上述問題。
關於 gRPC 和 Protobuf 的簡介可以看看這篇文章:Google Protocol Buffer 和 gRPC 簡介
gRPC & Protocol Buffer 實踐
我本地的 GOPATH 目錄為 /Users/hww/work/go
,給我們的 demo 項目新建一個目錄 cd $GOPATH/src && mkdir rpc-protobuf
定義 Protocol Buffer 的消息類型和服務
在項目根目錄 rpc-protobuf
新建文件目錄 customer
。首先給 Protocol Bufffer 文件定義服務介面和 paylaod 信息的數據結構,$GOPATH/scr/rpc-protobuf/customer/customer.proto
:
syntax = "proto3";package customer;// The Customer sercie definitionservice Customer { // Get all Customers with filter - A server-to-client streaming RPC. rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {} // Create a new Customer - A simple RPC rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}}message CustomerRequest { int32 id = 1; // Unique ID number for a Customer. string name = 2; string email = 3; string phone = 4; message Address { string street = 1; string city = 2; string state = 3; string zip = 4; bool isShippingAddress = 5; } repeated Address addresses = 5;}message CustomerResponse { int32 id = 1; bool success = 2;}message CustomerFilter { string keyword = 1;}
在 .proto
文件,第一行代碼為版本號,在這裡我們使用了 proto3 ;第二行代碼為包名,通過該文件生成的 Go 源碼包名和這裡的一致為 customer
我們定義了消息類型和服務介面。標準數據類型有 int32, float, double, 或 string 這些常見的類型。一種消息類型就是多個欄位的集合,每個欄位都被一個在該消息中唯一的整數標記;Customer
服務中有兩個 RPC 方法:
service Customer { // Get all Customers with filter - A server-to-client streaming RPC. rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {} // Create a new Customer - A simple RPC rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}}
解釋 Customer 服務之前,我們首先來大概瞭解一下 gRPC 中的三種類型的 RPC 方法。
- simple RPC應用於常見的典型的
Request/Response
模型。客戶端通過 stub 請求 RPC 的服務端並等待服務端的響應。 - Server-side streaming RPC客戶端給服務端發送一個請求並獲取服務端返回的流,用以讀取一連串的服務端響應。
stream
關鍵字在響應類型的前面。// 例子rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
- Client-side streaming RPC客戶端發送的請求 payload 有一連串的的信息,通過流給服務端發送請求。
stream
關鍵字在請求類型前面。// 例子
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
- Bidirectional streaming RPC服務端和客戶端之間都使用
read-write stream
進行通信。stream 關鍵字在請求類型和響應類型前面。// 例子rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}
理解 gRPC 提供的四種類型 RPC 方法之後,回到 Customer 的例子中。在 Customer 服務提供了兩種類型的 RPC 方法,分別是 simple RPC(CreateCustomer) 和 server-side streaming(GetCustomers) 。CreateCustomer 遵循標準 Request/Response 規範新建一個用戶;GetCustomers 方法中,服務端通過 stream 返回多個消費者信息的列表。
基於 proto 文件生成服務端和客戶端的 Go 代碼
定義好 proto 文件之後,然後生成你需要的編程語言源代碼,這些源代碼是服務端和客戶端業務邏輯代碼的介面。客戶端代碼通過消息類型和服務介面調用 RPC 方法。
protocol buffer 編譯器通過 gRPC 的 Go 插件生成客戶端和服務端的代碼。在項目根目錄下運行命令:protoc -I customer/ customer/customer.proto --go_out=plugins=grpc:customer
在 customer 目錄下生成了 customer.pb.go 文件。該源碼包含三大類功能:
- 讀寫和序列化請求和響應消息類型
- 提供定義在 proto 文件中定義的客戶端調用方法介面
- 提供定義在 proto 文件中定義的服務端實現方法介面
新建 gRPC 服務
以下代碼片段新建依據 proto 文件中定義的服務新建 gRPC 服務端。
// server/main.gopackage mainimport ( "log" "net" "strings" "golang.org/x/net/context" "google.golang.org/grpc" pb "rpc-protobuf/customer")const ( port = ":50051")// server is used to implement customer.CustomerServer.type server struct { savedCustomers []*pb.CustomerRequest}// CreateCustomer creates a new Customerfunc (s *server) CreateCustomer(ctx context.Context, in *pb.CustomerRequest) (*pb.CustomerResponse, error) { s.savedCustomers = append(s.savedCustomers, in) return &pb.CustomerResponse{Id: in.Id, Success: true}, nil}// GetCustomers returns all customers by given filterfunc (s *server) GetCustomers(filter *pb.CustomerFilter, stream pb.Customer_GetCustomersServer) error { for _, customer := range s.savedCustomers { if filter.Keyword != "" { if !strings.Contains(customer.Name, filter.Keyword) { continue } } if err := stream.Send(customer); err != nil { return err } } return nil}func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatal("failed to listen: %v", err) } //Create a new grpc server s := grpc.NewServer() pb.RegisterCustomerServer(s, &server{}) s.Serve(lis)}
服務端源碼中,server
結構體定義在 customer.pb.go 中的 CustomerServer 介面;CreateCustomer
和 GetCustomers
兩個方法定義在 customer.pb.go
文件的 CustomerClient 介面中。
CreateCustomer
是一個 simple rpc 類型的 RPC 方法,在這裡它接受兩個參數,分別是 context objct 和客戶端的請求信息,返回值為 proto 文件定義好的 CustomerResponse 對象;GetCustomers 是一個 server-side streaming 類型的 RPC 方法,接受兩個參數:CustomerRequest 對象、以及用來作為服務端對客戶端響應 stream 的 對象 Customer_GetCustomersServer 。
// Server API for Customer servicetype CustomerServer interface { // Get all Customers with filter - A server-to-client streaming RPC. GetCustomers(*CustomerFilter, Customer_GetCustomersServer) error // Create a new Customer - A simple RPC CreateCustomer(context.Context, *CustomerRequest) (*CustomerResponse, error)}
對比理解服務端代碼對兩個方法的實現,我們就可以理解參數的傳遞原理。
服務端代碼中 GetCustomers 方法內部有一行代碼 stream.Send(customer)
這個 Send 方法是 customer.pb.go 給 Customer_GetCustomersServer 介面定義並好的方法,表示給客戶端返回 stream
最後看看服務端代碼中的 main 方法。
首先grpc.NewServer
函數新建一個 gRPC 服務端;然後調用 customer.pb.go 中的 RegisterCustomerServer(s *grpc.Server, srv CustomerServer)
函數註冊該服務:pb.RegisterCustomerServer(s, &server{})
;最後通過 gRPC 的 Golang API Server.Serve 監聽指定的埠號:s.Serve(lis)
,新建一個 ServerTransport
和 service goroutine
處理監聽的埠收到的請求。
新建 gRPC 客戶端
首先看 customer.pb.go 生成的客戶端調用方法介面部分的代碼:
// Client API for Customer servicetype CustomerClient interface { // Get all Customers with filter - A server-to-client streaming RPC. GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error) // Create a new Customer - A simple RPC CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error)}type customerClient struct { cc *grpc.ClientConn}func NewCustomerClient(cc *grpc.ClientConn) CustomerClient { return &customerClient{cc}}
*grpc.ClientConn
表示連接到 RPC 服務端的客戶端,NewCustomerClient
函數返回一個 customerClient
結構體對象。CustomerClient
介面定義了兩個能夠被客戶端服務調用的方法,另外我們可以在 customer.pb.go 看到給 customerClient 類型的結構體實現這兩個函數的方法,故客戶端對象能夠調用 GetCustomers
和 CreateCustomer
方法:
func (c *customerClient) GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error) { ...}...func (c *customerClient) CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error) { ...}
接著回到實現客戶端的源碼:
// client/main.gopackage mainimport ( "io" "log" "golang.org/x/net/context" "google.golang.org/grpc" pb "rpc-protobuf/customer")const ( address = "localhost:50051")// createCustomer calls the RPC method CreateCustomer of CustomerServerfunc createCustomer(client pb.CustomerClient, customer *pb.CustomerRequest) { resp, err := client.CreateCustomer(context.Background(), customer) if err != nil { log.Fatalf("Could not create Customer: %v", err) } if resp.Success { log.Printf("A new Customer has been added with id: %d", resp.Id) }}// GetCustomers calls the RPC method GetCustomers of CustomerServerfunc getCustomers(client pb.CustomerClient, filter *pb.CustomerFilter) { // calling the streaming API stream, err := client.GetCustomers(context.Background(), filter) if err != nil { log.Fatal("Error on get customers: %v", err) } for { customer, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatal("%v.GetCustomers(_) = _, %v", client, err) } log.Printf("Customer: %v", customer) }}func main() { // Set up a connection to the RPC server conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatal("did not connect: %v", err) } defer conn.Close() // creates a new CustomerClient client := pb.NewCustomerClient(conn) customer := &pb.CustomerRequest{ Id: 101, Name: "Shiju Varghese", Email: "[email protected]", Phone: "732-757-2923", Addresses: []*pb.CustomerRequest_Address{ &pb.CustomerRequest_Address{ Street: "1 Mission Street", City: "San Francisco", State: "CA", Zip: "94105", IsShippingAddress: false, }, &pb.CustomerRequest_Address{ Street: "Greenfield", City: "Kochi", State: "KL", Zip: "68356", IsShippingAddress: true, }, }, } // Create a new customer createCustomer(client, customer) customer = &pb.CustomerRequest{ Id: 102, Name: "Irene Rose", Email: "[email protected]", Phone: "732-757-2924", Addresses: []*pb.CustomerRequest_Address{ &pb.CustomerRequest_Address{ Street: "1 Mission Street", City: "San Francisco", State: "CA", Zip: "94105", IsShippingAddress: true, }, }, } // Create a new customer createCustomer(client, customer) //Filter with an empty Keyword filter := &pb.CustomerFilter{Keyword: ""} getCustomers(client, filter)}
客戶端需要建立 gRPC 通道(channel) 纔可與服務端建立通信,調用 RPC 方法。grpc.Dial
函數表示新建與 RPC 服務端的連接。Dial
函數在 gRPC golang 實現的庫中聲明代碼如下:
func Dial(target string, opts ...DialOption) (*ClientConn, error)
除了連接地址作為第一個參數外,還可以傳多個可選參數。這些可選參數表示鑒權校驗,例如 TLS 或者 JWT 。在這裡的 grpc.WithInsecure
表示客戶端連接的安全傳輸被禁用。
調用服務端的 RPC 方法前,首先需要新建客戶端 stub :
// creates a new CustomerClientclient := pb.NewCustomerClient(conn)
在例子中,通過調用 RPC CreateCustomer 方法新增了兩個 customer 數據 : createCustomer(client, customer)
;調用 RPC GetCustomers 方法獲取所有 customers 數據。
至此,我們已經簡單地實現了一套 gRPC 客戶端和服務端代碼。在項目根目錄下運行命令:
? rpc-protobuf (nohup go run server/main.go &) && go run client/main.goappending output to nohup.out2017/10/28 18:08:02 A new Customer has been added with id: 1012017/10/28 18:08:02 A new Customer has been added with id: 1022017/10/28 18:08:02 Customer: id:101 name:"Shiju Varghese" email:"[email protected]" phone:"732-757-2923" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" > addresses:<street:"Greenfield" city:"Kochi" state:"KL" zip:"68356" isShippingAddress:true >2017/10/28 18:08:02 Customer: id:102 name:"Irene Rose" email:"[email protected]" phone:"732-757-2924" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" isShippingAddress:true >
推薦閱讀: