gRPC amp; 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 definition 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 RPCrpc 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 RPCrpc 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 文件之后,然后生成你需要的編程語言源代碼,這些源代碼是服務端和客戶端業務邏輯代碼的接口??蛻舳舜a通過消息類型和服務接口調用 RPC 方法。
protocol buffer 編譯器通過 gRPC 的 Go 插件生成客戶端和服務端的代碼。在項目根目錄下運行命令:
在 customer 目錄下生成了 customer.pb.go 文件。該源碼包含三大類功能:
- 讀寫和序列化請求和響應消息類型
- 提供定義在 proto 文件中定義的客戶端調用方法接口
- 提供定義在 proto 文件中定義的服務端實現方法接口
新建 gRPC 服務
以下代碼片段新建依據 proto 文件中定義的服務新建 gRPC 服務端。
// server/main.go package 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 Customer func (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 filter func (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 servers := 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 。
看看 customer.pb.go 中對 CustomerServer 接口的定義:
對比理解服務端代碼對兩個方法的實現,我們就可以理解參數的傳遞原理。
服務端代碼中 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 RPCCreateCustomer(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.go package 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 CustomerServer func 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 CustomerServer func getCustomers(client pb.CustomerClient, filter *pb.CustomerFilter) {// calling the streaming APIstream, 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 serverconn, err := grpc.Dial(address, grpc.WithInsecure())if err != nil {log.Fatal("did not connect: %v", err)}defer conn.Close()// creates a new CustomerClientclient := pb.NewCustomerClient(conn)customer := &pb.CustomerRequest{Id: 101,Name: "Shiju Varghese",Email: "shiju@xyz.com",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 customercreateCustomer(client, customer)customer = &pb.CustomerRequest{Id: 102,Name: "Irene Rose",Email: "irene@xyz.com",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 customercreateCustomer(client, customer)//Filter with an empty Keywordfilter := &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 CustomerClient client := 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.go appending output to nohup.out 2017/10/28 18:08:02 A new Customer has been added with id: 101 2017/10/28 18:08:02 A new Customer has been added with id: 102 2017/10/28 18:08:02 Customer: id:101 name:"Shiju Varghese" email:"shiju@xyz.com" 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:"irene@xyz.com" phone:"732-757-2924" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" isShippingAddress:true > https://zhuanlan.zhihu.com/p/30624616總結
以上是生活随笔為你收集整理的gRPC amp; Protocol Buffer 构建高性能接口实践的全部內容,希望文章能夠幫你解決所遇到的問題。