您好,登錄后才能下訂單哦!
這篇文章主要介紹怎么用Golang構建gRPC服務,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
我們的示例是一個簡單的路線圖應用,客戶端可以獲取路線特征信息、創建他們的路線摘要,還可以與服務器或者其他客戶端交換比如交通狀態更新這樣的路線信息。
借助gRPC,我們可以在.proto文件中定義我們的服務,并以gRPC支持的任何語言來實現客戶端和服務器,客戶端和服務器又可以在從服務器到你自己的平板電腦的各種環境中運行-gRPC還會為你解決所有不同語言和環境之間通信的復雜性。我們還獲得了使用protocol buffer的所有優點,包括有效的序列化(速度和體積兩方面都比JSON更有效率),簡單的IDL(接口定義語言)和輕松的接口更新。
首先需要安裝gRPC golang版本的軟件包,同時官方軟件包的examples目錄里就包含了教程中示例路線圖應用的代碼。
$ go get google.golang.org/grpc
然后切換到grpc-go/examples/route_guide:
目錄:
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide
安裝protocol buffer編譯器 安裝編譯器最簡單的方式是去https://github.com/protocolbuffers/protobuf/releases 下載預編譯好的protoc二進制文件,倉庫中可以找到每個平臺對應的編譯器二進制文件。這里我們以Mac Os為例,從https://github.com/protocolbuffers/protobuf/releases/download/v3.6.0/protoc-3.6.0-osx-x86_64.zip 下載并解壓文件。 更新PATH
系統變量,或者確保protoc
放在了PATH
包含的目錄中了。
安裝protoc編譯器插件
$ go get -u github.com/golang/protobuf/protoc-gen-go
編譯器插件protoc-gen-go
將安裝在$GOBIN
中,默認位于$GOPATH/bin
。編譯器protoc
必須在$PATH
中能找到它:
$ export PATH=$PATH:$GOPATH/bin
首先第一步是使用protocol buffer定義gRPC服務還有方法的請求和響應類型,你可以在下載的示例代碼examples/route_guide/routeguide/route_guide.proto中看到完整的.proto文件。
要定義服務,你需要在.proto
文件中指定一個具名的service
service RouteGuide { ... }
然后在服務定義中再來定義rpc
方法,指定他們的請求和響應類型。gRPC允許定義四種類型的服務方法,這四種服務方法都會應用到我們的RouteGuide
服務中。
一個簡單的RPC,客戶端使用存根將請求發送到服務器,然后等待響應返回,就像普通的函數調用一樣。
// 獲得給定位置的特征 rpc GetFeature(Point) returns (Feature) {}
服務器端流式RPC,客戶端向服務器發送請求,并獲取流以讀取回一系列消息。客戶端從返回的流中讀取,直到沒有更多消息為止。如我們的示例所示,可以通過將stream關鍵字放在響應類型之前來指定服務器端流方法。
//獲得給定Rectangle中可用的特征。結果是 //流式傳輸而不是立即返回 //因為矩形可能會覆蓋較大的區域并包含大量特征。 rpc ListFeatures(Rectangle) returns (stream Feature) {}
客戶端流式RPC,其中客戶端使用gRPC提供的流寫入一系列消息并將其發送到服務器。客戶端寫完消息后,它將等待服務器讀取所有消息并返回其響應。通過將stream關鍵字放在請求類型之前,可以指定客戶端流方法。
// 接收路線上被穿過的一系列點位, 當行程結束時 // 服務端會返回一個RouteSummary類型的消息. rpc RecordRoute(stream Point) returns (RouteSummary) {}
雙向流式RPC,雙方都使用讀寫流發送一系列消息。這兩個流是獨立運行的,因此客戶端和服務器可以按照自己喜歡的順序進行讀寫:例如,服務器可以在寫響應之前等待接收所有客戶端消息,或者可以先讀取消息再寫入消息,或其他一些讀寫組合。每個流中的消息順序都會保留。您可以通過在請求和響應之前都放置stream關鍵字來指定這種類型的方法。
//接收路線行進中發送過來的一系列RouteNotes類型的消息,同時也接收其他RouteNotes(例如:來自其他用戶) rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
我們的.proto
文件中也需要所有請求和響應類型的protocol buffer消息類型定義。比如說下面的Point消息類型:
// Points被表示為E7表示形式中的經度-緯度對。 //(度數乘以10 ** 7并四舍五入為最接近的整數)。 // 緯度應在+/- 90度范圍內,而經度應在 // 范圍+/- 180度(含) message Point { int32 latitude = 1; int32 longitude = 2; }
接下來要從我們的.proto
服務定義生成gRPC客戶端和服務端的接口。我們使用protoc
編譯器和上面安裝的編譯器插件來完成這些工作:
在示例route_guide
的目錄下運行:
protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide
運行命令后會在示例route_guide
目錄的routeguide
目錄下生成route_guide.pb.go
文件。
pb.go
文件里面包含:
用于填充、序列化和檢索我們定義的請求和響應消息類型的所有protocol buffer代碼。
一個客戶端存根用來讓客戶端調用RouteGuide
服務中定義的方法。
一個需要服務端實現的接口類型RouteGuideServer
,接口類型中包含了RouteGuide
服務中定義的所有方法。
首先讓我們看一下怎么創建RouteGuide
服務器。有兩種方法來讓我們的RouteGuide
服務工作:
實現我們從服務定義生成的服務接口:做服務實際要做的事情。
運行一個gRPC服務器監聽客戶端的請求然后把請求派發給正確的服務實現。 你可以在剛才安裝的gPRC包的grpc-go/examples/route_guide/server/server.go找到我們示例中RouteGuide`服務的實現代碼。下面讓我們看看他是怎么工作的。
如你所見,實現代碼中有一個routeGuideServer
結構體類型,它實現了protoc
編譯器生成的pb.go
文件中定義的RouteGuideServer
接口。
type routeGuideServer struct { ... } ... func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) { ... } ... func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error { ... } ... func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error { ... } ... func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error { ... } ...
routeGuideServer
實現我們所有的服務方法。首先,讓我們看一下最簡單的類型GetFeature,它只是從客戶端獲取一個Point
,并從其Feature
數據庫中返回相應的Feature
信息。
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) { for _, feature := range s.savedFeatures { if proto.Equal(feature.Location, point) { return feature, nil } } // No feature was found, return an unnamed feature return &pb.Feature{"", point}, nil }
這個方法傳遞了RPC上下文對象和客戶端的Point
protocol buffer請求消息,它在響應信息中返回一個Feature
類型的protocol buffer消息和錯誤。在該方法中,我們使用適當的信息填充Feature
,然后將其返回并返回nil錯誤,以告知gRPC我們已經完成了RPC的處理,并且可以將Feature
返回給客戶端。
現在,讓我們看一下服務方法中的一個流式RPC。 ListFeatures
是服務器端流式RPC,因此我們需要將多個Feature
發送回客戶端。
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error { for _, feature := range s.savedFeatures { if inRange(feature.Location, rect) { if err := stream.Send(feature); err != nil { return err } } } return nil }
如你所見,這次我們沒有獲得簡單的請求和響應對象,而是獲得了一個請求對象(客戶端要在其中查找Feature
的Rectangle
)和一個特殊的RouteGuide_ListFeaturesServer
對象來寫入響應。
在該方法中,我們填充了需要返回的所有Feature
對象,并使用Send()方
法將它們寫入RouteGuide_ListFeaturesServer
。最后,就像在簡單的RPC中一樣,我們返回nil
錯誤來告訴gRPC我們已經完成了響應的寫入。如果此調用中發生任何錯誤,我們將返回非nil
錯誤; gRPC層會將其轉換為適當的RPC狀態,以在線上發送。
現在,讓我們看一些更復雜的事情:客戶端流方法RecordRoute
,從客戶端獲取點流,并返回一個包含行程信息的RouteSummary
。如你所見,這一次該方法根本沒有request參數。相反,它獲得一個RouteGuide_RecordRouteServer
流,服務器可以使用該流來讀取和寫入消息-它可以使用Recv()
方法接收客戶端消息,并使用SendAndClose()
方法返回其單個響應。
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error { var pointCount, featureCount, distance int32 var lastPoint *pb.Point startTime := time.Now() for { point, err := stream.Recv() if err == io.EOF { endTime := time.Now() return stream.SendAndClose(&pb.RouteSummary{ PointCount: pointCount, FeatureCount: featureCount, Distance: distance, ElapsedTime: int32(endTime.Sub(startTime).Seconds()), }) } if err != nil { return err } pointCount++ for _, feature := range s.savedFeatures { if proto.Equal(feature.Location, point) { featureCount++ } } if lastPoint != nil { distance += calcDistance(lastPoint, point) } lastPoint = point } }
在方法主體中,我們使用RouteGuide_RecordRouteServer
的Recv()
方法不停地讀取客戶端的請求到一個請求對象中(在本例中為Point
),直到沒有更多消息為止:服務器需要要在每次調用后檢查從Recv()
返回的錯誤。如果為nil
,則流仍然良好,并且可以繼續讀取;如果是io.EOF,則表示消息流已結束,服務器可以返回其RouteSummary。如果錯誤為其他值,我們將返回錯誤“原樣”,以便gRPC層將其轉換為RPC狀態。
最后讓我們看一下雙向流式RPC方法RouteChat()
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error { for { in, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } key := serialize(in.Location) s.mu.Lock() s.routeNotes[key] = append(s.routeNotes[key], in) // Note: this copy prevents blocking other clients while serving this one. // We don't need to do a deep copy, because elements in the slice are // insert-only and never modified. rn := make([]*pb.RouteNote, len(s.routeNotes[key])) copy(rn, s.routeNotes[key]) s.mu.Unlock() for _, note := range rn { if err := stream.Send(note); err != nil { return err } } } }
這次,我們得到一個RouteGuide_RouteChatServer
流,就像在客戶端流示例中一樣,該流可用于讀取和寫入消息。但是,這次,當客戶端仍在向其消息流中寫入消息時,我們會向流中寫入要返回的消息。
此處的讀寫語法與我們的客戶端流式傳輸方法非常相似,不同之處在于服務器使用流的Send()
方法而不是SendAndClose()
,因為服務器會寫入多個響應。盡管雙方總是會按照對方的寫入順序來獲取對方的消息,但是客戶端和服務器都可以以任意順序進行讀取和寫入-流完全獨立地運行(意思是服務器可以接受完請求后再寫流,也可以接收一條請求寫一條響應。同樣的客戶端可以寫完請求了再讀響應,也可以發一條請求讀一條響應)
一旦實現了所有方法,我們還需要啟動gRPC服務器,以便客戶端可以實際使用我們的服務。以下代碼段顯示了如何啟動RouteGuide
服務。
flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } grpcServer := grpc.NewServer() pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{}) ... // determine whether to use TLS grpcServer.Serve(lis)
為了構建和啟動服務器我們需要:
指定要監聽客戶端請求的接口lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
。
使用grpc.NewServer()
創建一個gRPC server的實例。
使用gRPC server注冊我們的服務實現。
使用我們的端口詳細信息在服務器上調用Serve()
進行阻塞等待,直到進程被殺死或調用Stop()
為止。
在這一部分中我們將為RouteGuide服務創建Go客戶端,你可以在grpc-go/examples/route_guide/client/client.go 看到完整的客戶端代碼。
要調用服務的方法,我們首先需要創建一個gRPC通道與服務器通信。我們通過把服務器地址和端口號傳遞給grpc.Dial()
來創建通道,像下面這樣:
conn, err := grpc.Dial(*serverAddr) if err != nil { ... } defer conn.Close()
如果你請求的服務需要認證,你可以在grpc.Dial
中使用DialOptions
設置認證憑證(比如:TLS,GCE憑證,JWT憑證)--不過我們的RouteGuide
服務不需要這些。
設置gRPC通道后,我們需要一個客戶端存根來執行RPC。我們使用從.proto
生成的pb
包中提供的NewRouteGuideClient
方法獲取客戶端存根。
client := pb.NewRouteGuideClient(conn)
生成的pb.go
文件定義了客戶端接口類型RouteGuideClient
并用客戶端存根的結構體類型實現了接口中的方法,所以通過上面獲取到的客戶端存根client
可以直接調用下面接口類型中列出的方法。
type RouteGuideClient interface { GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) }
每個實現方法會再去請求gRPC服務端相對應的方法獲取服務端的響應,比如:
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) { out := new(Feature) err := c.cc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, opts...) if err != nil { return nil, err } return out, nil }
RouteGuideClient
接口的完整實現可以在生成的pb.go
文件里找到。
現在讓我們看看如何調用服務的方法。注意在gRPC-Go中,PRC是在阻塞/同步模式下的運行的,也就是說RPC調用會等待服務端響應,服務端將返回響應或者是錯誤。
調用普通RPC方法GetFeature如同直接調用本地的方法。
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906}) if err != nil { ... }
如你所見,我們在之前獲得的存根上調用該方法。在我們的方法參數中,我們創建并填充一個protocol buffer對象(在本例中為Point對象)。我們還會傳遞一個context.Context
對象,該對象可讓我們在必要時更改RPC的行為,例如超時/取消正在調用的RPC(cancel an RPC in flight)。如果調用沒有返回錯誤,則我們可以從第一個返回值中讀取服務器的響應信息。
這里我們會調用服務端流式方法ListFeatures
,方法返回的流中包含了地理特征信息。如果你讀過上面的創建客戶端的章節,這里有些東西看起來會很熟悉--流式RPC在兩端實現的方式很類似。
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle stream, err := client.ListFeatures(context.Background(), rect) if err != nil { ... } for { feature, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatalf("%v.ListFeatures(_) = _, %v", client, err) } log.Println(feature) }
和簡單RPC調用一樣,調用時傳遞了一個方法的上下文和一個請求。但是我們取回的是一個RouteGuide_ListFeaturesClient
實例而不是一個響應對象。客戶端可以使用RouteGuide_ListFeaturesClient
流讀取服務器的響應。
我們使用RouteGuide_ListFeaturesClient
的Recv()
方法不停地將服務器的響應讀入到一個protocol buffer響應對象中(本例中的Feature
對象),直到沒有更多消息為止:客戶端需要在每次調用后檢查從Recv()
返回的錯誤err
。如果為nil
,則流仍然良好,并且可以繼續讀取;如果是io.EOF
,則消息流已結束;否則就是一定RPC錯誤,該錯誤會通過err
傳遞給調用程序。
客戶端流方法RecordRoute
與服務器端方法相似,不同之處在于,我們僅向該方法傳遞一個上下文并獲得一個RouteGuide_RecordRouteClient
流,該流可用于寫入和讀取消息。
// 隨機的創建一些Points r := rand.New(rand.NewSource(time.Now().UnixNano())) pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points var points []*pb.Point for i := 0; i < pointCount; i++ { points = append(points, randomPoint(r)) } log.Printf("Traversing %d points.", len(points)) stream, err := client.RecordRoute(context.Background())// 調用服務中定義的客戶端流式RPC方法 if err != nil { log.Fatalf("%v.RecordRoute(_) = _, %v", client, err) } for _, point := range points { if err := stream.Send(point); err != nil {// 向流中寫入多個請求消息 if err == io.EOF { break } log.Fatalf("%v.Send(%v) = %v", stream, point, err) } } reply, err := stream.CloseAndRecv()// 從流中取回服務器的響應 if err != nil { log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil) } log.Printf("Route summary: %v", reply)
RouteGuide_RecordRouteClient
有一個Send()
。我們可以使用它發送請求給服務端。一旦我們使用Send()
寫入流完成后,我們需要在流上調用CloseAndRecv()
方法讓gRPC知道我們已經完成了請求的寫入并且期望得到一個響應。我們從CloseAndRecv()
方法返回的err中可以獲得RPC狀態。如果狀態是nil
,CloseAndRecv()
的第一個返回值就是一個有效的服務器響應。
最后,讓我們看一下雙向流式RPC RouteChat()
。與RecordRoute
一樣,我們只向方法傳遞一個上下文對象,然后獲取一個可用于寫入和讀取消息的流。但是,這一次我們在服務器仍將消息寫入消息流的同時,通過方法的流返回值。
stream, err := client.RouteChat(context.Background()) waitc := make(chan struct{}) go func() { for { in, err := stream.Recv() if err == io.EOF { // read done. close(waitc) return } if err != nil { log.Fatalf("Failed to receive a note : %v", err) } log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude) } }() for _, note := range notes { if err := stream.Send(note); err != nil { log.Fatalf("Failed to send a note: %v", err) } } stream.CloseSend() <-waitc
除了在完成調用后使用流的CloseSend()
方法外,此處的讀寫語法與我們的客戶端流方法非常相似。盡管雙方總是會按照對方的寫入順序來獲取對方的消息,但是客戶端和服務器都可以以任意順序進行讀取和寫入-兩端的流完全獨立地運行。
要編譯和運行服務器,假設你位于$GOPATH/src/google.golang.org/grpc/examples/route_guide
文件夾中,只需:
$ go run server/server.go
同樣,運行客戶端:
$ go run client/client.go
你將看到如下輸出:
Getting feature for point (409146138, -746188906) name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 > Getting feature for point (0, 0) location:<> Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 > name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 > ... name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 > Traversing 56 points. Route summary: point_count:56 distance:497013163 Got message First message at point(0, 1) Got message Second message at point(0, 2) Got message Third message at point(0, 3) Got message First message at point(0, 1) Got message Fourth message at point(0, 1) Got message Second message at point(0, 2) Got message Fifth message at point(0, 2) Got message Third message at point(0, 3) Got message Sixth message at point(0, 3)
以上是“怎么用Golang構建gRPC服務”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。