gRPC入門
—— 時間飛逝如一名攜帶資訊的郵差但那隻不過是我們的比喻人物是杜撰的匆忙是假裝的攜帶的也不是人的訊息
為什麼使用grpc
主要包括以下兩點原因:
protocl buffer http 2.0
很對人經常拿
thrift
跟
grpc
比較,現在先不發表任何看法,後續會深入thrift
進行介紹。
http/2
The resulting protocol is more friendly to the network, because fewer TCP connections can be used in comparison to HTTP/1.x. This means less competition with other flows, and longer-lived connections, which in turn leads to better utilization of available network capacity. Finally, HTTP/2 also enables more efficient processing of messages through use of binary message framing.
http/2
帶來了網路效能的巨大提升,下面列舉一些個人覺得比較重要的細節:
http/2
更多細節,請參考文章末尾的連結,當然,後續也會專門介紹。
準備工作
大家可以參考
protobuf
的介紹,具體包括:
-
安裝
Go
的開發環境,因為後續是基於Go
語言的開發專案 -
安裝
protocol-buffers
-
安裝
protoc-gen-go
,用於自動生成原始碼
生成原始碼的命令如下,其中,--go_out
用於指定生成原始碼的儲存路徑;而-I
是-IPATH
的簡寫,用於指定查詢import
檔案的路徑,可以指定多個;最後的order
是編譯的grpc
檔案的儲存路徑。
protoc -I proto/ proto/order.proto --go_out=plugins=grpc:order
protocol buffer
google
開發的高效、跨平臺的資料傳輸格式。當然,本質還是資料傳輸結構。但google
賦予了它豐富的功能,比如import
、package
、訊息巢狀等等。import
用於引入別的.proto
檔案;package
用於定義名稱空間,轉換到go
原始碼中就是包名;repeated
用於定義重複的資料;enum
用於定義列舉型別等。
.proto
內欄位的基本定義:
type name = tag;
Protocol buffer
本身不包含型別的描述資訊,因此獲取了沒有.proto
描述檔案的二進位制資訊是毫無用處的,我們很難提取出非常有用的資訊。Go
語言complier
生成的檔案字尾是.pb.go
,它自動生成了set
、get
以及read
、write
方法,我們可以很方便的序列化資料。
下面我們定義一個建立訂單的.proto
檔案,概括的描述:buyerID
在device
上支付amount
買
sku
商品。
-
宣告版本為
proto3
,package
是order
。 -
裝置型別定義為列舉型別,包括
ANDROID
和IOS
兩種,而且型別被巢狀宣告在OrderParams
內。 -
sku
宣告為repeated
,因為使用者可能購買多個商品。 -
OrderResult
為響應的訊息體結構,包括生成的訂單號和處理的響應碼。 -
service
聲明瞭order
要提供的服務。當前僅僅實現一個simple RPC
:客戶端使用OrderParams
引數請求RPC
伺服器,收到OrderResult
作為響應。
syntax = "proto3"; package order; service Order { //a simple RPC //create new order rpc Add (OrderParams) returns (OrderResult) { } } message OrderParams { string amount = 1; //訂單金額 int64 buyerID = 2; //購買使用者ID enum Device { IOS = 0; ANDROID = 1; } Device device = 3; repeated Sku sku = 4; } message Sku { int32 num = 1; string skuId = 2; int32 unitPrice = 3; } message OrderResult { int32 statusCode = 1; string orderID = 2; }
grpc
介面
通過定義的.proto
檔案生成grpc client
和server
端實現的介面型別。生成的內容主要包括:
-
protocol buffer
各種訊息型別的序列化操作 -
grpc client
實現的介面型別,以及client
實現的grpc
方法 -
grpc server
待實現的介面型別
service
處理流程
第一步. 服務端為每個接收的連線建立單獨的goroutine
進行處理。
第二步. 自動生成的程式碼中,聲明瞭服務的具體描述,也是該服務的“路由”。包括服務名稱ServiceName
以Methods
、Streams
。當rpc
接收到新的資料時,會根據路由執行對應的方法。因為我們的設定沒有處理流的場景,所以Streams
為空的結構體。
程式碼中的服務名稱被指定為:order.Order
,對應建立訂單的方法是:Add
。
var _Order_serviceDesc = grpc.ServiceDesc{ ServiceName: "order.Order", HandlerType: (*OrderServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "Add", Handler:_Order_Add_Handler, }, }, Streams:[]grpc.StreamDesc{}, Metadata: "order.proto", }
第三步. 將路由註冊到rpc
服務中。如下所示,就是將上述的路由轉換為map
對應關係的過程。類比restful
風格的介面定義,等價於/order/
這種請求都由這個service
來進行處理。
最終將service
註冊到gRPC server
上。同時,我們可以逆向猜出服務的處理過程:通過請求的路徑獲取service
,然後通過MethodName
呼叫相應的處理方法。
srv := &service{ server: ss, md:make(map[string]*MethodDesc), sd:make(map[string]*StreamDesc), mdata:sd.Metadata, } for i := range sd.Methods { d := &sd.Methods[i] srv.md[d.MethodName] = d } for i := range sd.Streams { d := &sd.Streams[i] srv.sd[d.StreamName] = d } s.m[sd.ServiceName] = srv
第四步.gRPC
服務處理請求。通過請求的:path
,獲取對應的service
和MethodName
進行處理。
service := sm[:pos] method := sm[pos+1:] if srv, ok := s.m[service]; ok { if md, ok := srv.md[method]; ok { s.processUnaryRPC(t, stream, srv, md, trInfo) return } if sd, ok := srv.sd[method]; ok { s.processStreamingRPC(t, stream, srv, sd, trInfo) return } }
通過結合protoc
自動生成的client
端程式碼,無需抓包,我們就可以推斷出path
的格式,以及系統是如何處理路由的。程式碼中定義的:/order.Order/Add
就是依據。
func (c *orderClient) Add(ctx context.Context, in *OrderParams, opts ...grpc.CallOption) (*OrderResult, error) { out := new(OrderResult) err := c.cc.Invoke(ctx, "/order.Order/Add", in, out, opts...) if err != nil { return nil, err } return out, nil }
建立訂單
為了簡單起見,我們只保證訂單的唯一性。這裡我們實現一個簡易版本,而且也不做過多介紹。感興趣的同學可以移步到另一篇文章:探討分散式ID生成系統 去了解,畢竟不應該是本節的重心。
//上次建立訂單使用的毫秒時間 var lastTimestamp = time.Now().UnixNano() / 1000000 var sequence int64 const MaxSequence = 4096 // 42bit分配給毫秒時間戳 // 12bit分配給序列號,每4096就重新開始迴圈 // 10bit分配給機器ID func CreateOrder(nodeId int64) string { currentTimestamp := getCurrentTimestamp() if currentTimestamp == lastTimestamp { sequence = (sequence + 1) % MaxSequence if sequence == 0 { currentTimestamp = waitNextMillis(currentTimestamp) } } else { sequence = 0 } orderId := currentTimestamp << 22 orderId |= nodeId << 10 orderId |= sequence return strings.ToUpper(fmt.Sprintf("%x", orderId)) } func getCurrentTimestamp() int64 { return time.Now().UnixNano() / 1000000 } func waitNextMillis(currentTimestamp int64) int64 { for currentTimestamp == lastTimestamp { currentTimestamp = getCurrentTimestamp() } return currentTimestamp }
執行系統
建立服務端程式碼。注意:使用grpc
提供的預設選項,其實是很危險的行為。在生產開發中,被不熟悉的預設選項坑到的情況比比皆是。這裡的程式碼不要作為後續生產環境開發的參考。服務端的程式碼相比客戶端要複雜一點,需要我們去實現處理請求的介面。
type Order struct { } func (o *Order) Add(ctx context.Context, in *order.OrderParams) (*order.OrderResult, error) { return &order.OrderResult{ OrderID: util.CreateOrder(1), }, nil } func main() { lis, err := net.Listen("tcp", "127.0.0.1:10000") if err != nil { log.Fatalf("Failed to listen: %v", err) } grpcServer := grpc.NewServer() order.RegisterOrderServer(grpcServer, &Order{}) grpcServer.Serve(lis) }
客戶端的程式碼非常簡單,構造引數,處理返回就Ok
了。
func createOrder(client order.OrderClient, params *order.OrderParams) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() orderResult, err := client.Add(ctx, params) if err != nil { log.Fatalf("%v.GetFeatures(_) = _, %v: ", client, err) } log.Println(orderResult) } func main() { conn, err := grpc.Dial("127.0.0.1:10000") if err != nil { log.Fatalf("fail to dial: %v", err) } defer conn.Close() client := order.NewOrderClient(conn) orderParams := &order.OrderParams{ BuyerID: 10318003, } createOrder(client, orderParams) }
總結
文章介紹了gRPC
的入門知識,包括protocol buffer
以及http/2
,gRPC
封裝了很多東西,對於一般場合,我們只需要指定配置,實現介面就可以了,非常簡單。
在入門的介紹裡,大家會覺得gRPC
不就跟RESTFUL
請求一樣嗎?確實是,我也這樣覺得。但存在一個最直觀的優點:通過使用gRPC
,可以將複雜的介面呼叫關係封裝在SDK
中,直接提供給第三方使用,而且還能有效避免錯誤呼叫介面的情況。
如果gRPC
只能這樣的話,它就太失敗了,他用HTTP/2
簡直就是用來打蚊子的,讓我們後續繼續深入瞭解吧。