gRPC入门
—— 時(shí)間飛逝 如一名攜帶信息的郵差 但那只不過(guò)是我們的比喻 人物是杜撰的 匆忙是假裝的 攜帶的也不是人的訊息
為什么使用grpc
主要包括以下兩點(diǎn)原因:
很對(duì)人經(jīng)常拿thrift跟grpc比較,現(xiàn)在先不發(fā)表任何看法,后續(xù)會(huì)深入thrift進(jìn)行介紹。
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帶來(lái)了網(wǎng)絡(luò)性能的巨大提升,下面列舉一些個(gè)人覺(jué)得比較重要的細(xì)節(jié):
更多細(xì)節(jié),請(qǐng)參考文章末尾的鏈接,當(dāng)然,后續(xù)也會(huì)專(zhuān)門(mén)介紹。
準(zhǔn)備工作
大家可以參考protobuf的介紹,具體包括:
生成源碼的命令如下,其中,--go_out用于指定生成源碼的保存路徑;而-I是-IPATH的簡(jiǎn)寫(xiě),用于指定查找import文件的路徑,可以指定多個(gè);最后的order是編譯的grpc文件的存儲(chǔ)路徑。
protoc -I proto/ proto/order.proto --go_out=plugins=grpc:orderprotocol buffer
google開(kāi)發(fā)的高效、跨平臺(tái)的數(shù)據(jù)傳輸格式。當(dāng)然,本質(zhì)還是數(shù)據(jù)傳輸結(jié)構(gòu)。但google賦予了它豐富的功能,比如import、package、消息嵌套等等。import用于引入別的.proto文件;package用于定義命名空間,轉(zhuǎn)換到go源碼中就是包名;repeated用于定義重復(fù)的數(shù)據(jù);enum用于定義枚舉類(lèi)型等。
.proto內(nèi)字段的基本定義:
type name = tag;Protocol buffer本身不包含類(lèi)型的描述信息,因此獲取了沒(méi)有.proto描述文件的二進(jìn)制信息是毫無(wú)用處的,我們很難提取出非常有用的信息。Go語(yǔ)言complier生成的文件后綴是.pb.go,它自動(dòng)生成了set、get以及read、write方法,我們可以很方便的序列化數(shù)據(jù)。
下面我們定義一個(gè)創(chuàng)建訂單的.proto文件,概括的描述:buyerID在device上支付amount買(mǎi)sku商品。
grpc接口
通過(guò)定義的.proto文件生成grpc client和server端實(shí)現(xiàn)的接口類(lèi)型。生成的內(nèi)容主要包括:
service處理流程
第一步. 服務(wù)端為每個(gè)接收的連接創(chuàng)建單獨(dú)的goroutine進(jìn)行處理。
第二步. 自動(dòng)生成的代碼中,聲明了服務(wù)的具體描述,也是該服務(wù)的“路由”。包括服務(wù)名稱(chēng)ServiceName以Methods、Streams。當(dāng)rpc接收到新的數(shù)據(jù)時(shí),會(huì)根據(jù)路由執(zhí)行對(duì)應(yīng)的方法。因?yàn)槲覀兊脑O(shè)定沒(méi)有處理流的場(chǎng)景,所以Streams為空的結(jié)構(gòu)體。
代碼中的服務(wù)名稱(chēng)被指定為:order.Order,對(duì)應(yīng)創(chuàng)建訂單的方法是: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", }第三步. 將路由注冊(cè)到rpc服務(wù)中。如下所示,就是將上述的路由轉(zhuǎn)換為map對(duì)應(yīng)關(guān)系的過(guò)程。類(lèi)比restful風(fēng)格的接口定義,等價(jià)于/order/這種請(qǐng)求都由這個(gè)service來(lái)進(jìn)行處理。
最終將service注冊(cè)到gRPC server上。同時(shí),我們可以逆向猜出服務(wù)的處理過(guò)程:通過(guò)請(qǐng)求的路徑獲取service,然后通過(guò)MethodName調(diào)用相應(yīng)的處理方法。
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服務(wù)處理請(qǐng)求。通過(guò)請(qǐng)求的:path,獲取對(duì)應(yīng)的service和MethodName進(jìn)行處理。
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} }通過(guò)結(jié)合protoc自動(dòng)生成的client端代碼,無(wú)需抓包,我們就可以推斷出path的格式,以及系統(tǒng)是如何處理路由的。代碼中定義的:/order.Order/Add就是依據(jù)。
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 }創(chuàng)建訂單
為了簡(jiǎn)單起見(jiàn),我們只保證訂單的唯一性。這里我們實(shí)現(xiàn)一個(gè)簡(jiǎn)易版本,而且也不做過(guò)多介紹。感興趣的同學(xué)可以移步到另一篇文章:探討分布式ID生成系統(tǒng)去了解,畢竟不應(yīng)該是本節(jié)的重心。
//上次創(chuàng)建訂單使用的毫秒時(shí)間 var lastTimestamp = time.Now().UnixNano() / 1000000 var sequence int64const MaxSequence = 4096// 42bit分配給毫秒時(shí)間戳 // 12bit分配給序列號(hào),每4096就重新開(kāi)始循環(huán) // 10bit分配給機(jī)器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 }運(yùn)行系統(tǒng)
創(chuàng)建服務(wù)端代碼。注意:使用grpc提供的默認(rèn)選項(xiàng),其實(shí)是很危險(xiǎn)的行為。在生產(chǎn)開(kāi)發(fā)中,被不熟悉的默認(rèn)選項(xiàng)坑到的情況比比皆是。這里的代碼不要作為后續(xù)生產(chǎn)環(huán)境開(kāi)發(fā)的參考。服務(wù)端的代碼相比客戶(hù)端要復(fù)雜一點(diǎn),需要我們?nèi)?shí)現(xiàn)處理請(qǐng)求的接口。
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) }客戶(hù)端的代碼非常簡(jiǎn)單,構(gòu)造參數(shù),處理返回就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) }總結(jié)
文章介紹了gRPC的入門(mén)知識(shí),包括protocol buffer以及http/2,gRPC封裝了很多東西,對(duì)于一般場(chǎng)合,我們只需要指定配置,實(shí)現(xiàn)接口就可以了,非常簡(jiǎn)單。
在入門(mén)的介紹里,大家會(huì)覺(jué)得gRPC不就跟RESTFUL請(qǐng)求一樣嗎?確實(shí)是,我也這樣覺(jué)得。但存在一個(gè)最直觀的優(yōu)點(diǎn):通過(guò)使用gRPC,可以將復(fù)雜的接口調(diào)用關(guān)系封裝在SDK中,直接提供給第三方使用,而且還能有效避免錯(cuò)誤調(diào)用接口的情況。
如果gRPC只能這樣的話,它就太失敗了,他用HTTP/2簡(jiǎn)直就是用來(lái)打蚊子的,讓我們后續(xù)繼續(xù)深入了解吧。
參考文章:
總結(jié)
- 上一篇: 直播操作流程
- 下一篇: kylin KV+cube方案分析