gRPC入门
—— 時間飛逝 如一名攜帶信息的郵差 但那只不過是我們的比喻 人物是杜撰的 匆忙是假裝的 攜帶的也不是人的訊息
為什么使用grpc
主要包括以下兩點原因:
很對人經常拿thrift跟grpc比較,現在先不發表任何看法,后續會深入thrift進行介紹。
http/2
HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection… Specifically, it allows interleaving of request and response messages on the same connection and uses an efficient coding for HTTP header fields. It also allows prioritization of requests, letting more important requests complete more quickly, further improving performance.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帶來了網絡性能的巨大提升,下面列舉一些個人覺得比較重要的細節:
更多細節,請參考文章末尾的鏈接,當然,后續也會專門介紹。
準備工作
大家可以參考protobuf的介紹,具體包括:
生成源碼的命令如下,其中,--go_out用于指定生成源碼的保存路徑;而-I是-IPATH的簡寫,用于指定查找import文件的路徑,可以指定多個;最后的order是編譯的grpc文件的存儲路徑。
protoc -I proto/ proto/order.proto --go_out=plugins=grpc:orderprotocol 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商品。
grpc接口
通過定義的.proto文件生成grpc client和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 int64const MaxSequence = 4096// 42bit分配給毫秒時間戳 // 12bit分配給序列號,每4096就重新開始循環 // 10bit分配給機器ID func CreateOrder(nodeId int64) string {currentTimestamp := getCurrentTimestamp()if currentTimestamp == lastTimestamp {sequence = (sequence + 1) % MaxSequenceif sequence == 0 {currentTimestamp = waitNextMillis(currentTimestamp)}} else {sequence = 0}orderId := currentTimestamp << 22orderId |= nodeId << 10orderId |= sequencereturn 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簡直就是用來打蚊子的,讓我們后續繼續深入了解吧。
參考文章:
總結
- 上一篇: 直播操作流程
- 下一篇: kylin KV+cube方案分析