go语言入门经典_Go 语言中的 gRPC 基础入门
01
為什么使用 gRPC?
借助 gRPC,我們可以在 .proto 文件中一次定義我們的服務(wù),并以 gRPC 支持的任何語(yǔ)言生成客戶端和服務(wù)器代碼,無(wú)論是在大型數(shù)據(jù)中心內(nèi)的服務(wù)器,還是在個(gè)人的電腦的環(huán)境中,這些客戶端和服務(wù)器代碼都可以運(yùn)行 –? gRPC 可以為您處理不同語(yǔ)言和環(huán)境之間的通信。我們還獲得了使用 protocol buffers 的所有優(yōu)點(diǎn),包括有效的序列化,簡(jiǎn)單的 IDL 和容易的接口更新。
我們的示例是一個(gè)簡(jiǎn)單的路由映射應(yīng)用程序,它使客戶端可以獲取有關(guān)其路由功能的信息,創(chuàng)建其路由的摘要以及與服務(wù)器和其他客戶端交換路由信息(例如流量更新)。
02
準(zhǔn)備工作
安裝 Go 最新正式發(fā)行版本。
安裝 protocol buffers 編譯器 protoc。請(qǐng)參考「Protobuf - 更小、更快、更簡(jiǎn)單的交互式數(shù)據(jù)語(yǔ)言」- Part 05。
安裝編譯器 protoc 的 Go 插件。請(qǐng)參考「gRPC 初探與簡(jiǎn)單使用」- Part 04。
git clone 示例代碼,并進(jìn)入該目錄。
$ git clone https://github.com/grpc/grpc-go$ cd grpc-go/examples/route_guide
03
定義服務(wù)并生成客戶端和服務(wù)器代碼
我們的第一步是使用 protocol buffers 定義 gRPC 服務(wù)以及方法請(qǐng)求和響應(yīng)類型。
有關(guān)完整的
.proto 文件,請(qǐng)參閱?Part 2?git clone 的代碼 routeguide/route_guide.proto。
要定義服務(wù),請(qǐng)?jiān)?.proto 文件中指定一個(gè)命名服務(wù):
service RouteGuide { ...}然后,在服務(wù)定義中定義 rpc 方法,并指定它們的請(qǐng)求和響應(yīng)類型。gRPC 允許您定義四種服務(wù)方法,所有這些方法都在 RouteGuide 服務(wù)中使用:
一個(gè)簡(jiǎn)單的 RPC,客戶端使用存根將請(qǐng)求發(fā)送到服務(wù)器,然后等待響應(yīng)返回,就像正常的函數(shù)調(diào)用一樣。
// Obtains the feature at a given position.rpc GetFeature(Point) returns (Feature) {}服務(wù)器端流式 RPC,客戶端在其中向服務(wù)器發(fā)送請(qǐng)求,并獲取流以讀取回一系列消息。客戶端從返回的流中讀取,直到?jīng)]有更多消息為止。如我們的示例所示,您可以通過(guò)在響應(yīng)類型之前放置?stream 關(guān)鍵字來(lái)指定服務(wù)器端流方法。
// Obtains the Features available within the given Rectangle. Results are// streamed rather than returned at once (e.g. in a response message with a// repeated field), as the rectangle may cover a large area and contain a// huge number of features.rpc ListFeatures(Rectangle) returns (stream Feature) {}客戶端流式 RPC,客戶端在其中編寫消息序列,然后再次使用提供的流將其發(fā)送到服務(wù)器。客戶端寫完消息后,它將等待服務(wù)器讀取所有消息并返回其響應(yīng)。通過(guò)將?stream 關(guān)鍵字放在請(qǐng)求類型之前,可以指定客戶端流方法。
// Accepts a stream of Points on a route being traversed, returning a// RouteSummary when traversal is completed.rpc RecordRoute(stream Point) returns (RouteSummary) {}雙向流式 RPC,雙方都使用讀寫流發(fā)送一系列消息。這兩個(gè)流是獨(dú)立運(yùn)行的,因此客戶端和服務(wù)器可以按照自己喜歡的順序進(jìn)行讀寫:例如,服務(wù)器可以在寫響應(yīng)之前等待接收所有客戶端消息,或者可以先讀取一條消息再寫入一條消息,或讀寫的其他組合。每個(gè)流中的消息順序都會(huì)保留。您可以通過(guò)在請(qǐng)求和響應(yīng)之前都放置?stream 關(guān)鍵字來(lái)指定這種類型的方法。
// Accepts a stream of RouteNotes sent while a route is being traversed,// while receiving other RouteNotes (e.g. from other users).rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}我們的 .proto 文件還包含用于服務(wù)方法中所有請(qǐng)求和響應(yīng)類型的 protocol buffers message 類型定義-例如,這是 Point message 類型:
// Points are represented as latitude-longitude pairs in the E7 representation// (degrees multiplied by 10**7 and rounded to the nearest integer).// Latitudes should be in the range +/- 90 degrees and longitude should be in// the range +/- 180 degrees (inclusive).message Point { int32 latitude = 1; int32 longitude = 2;}接下來(lái),我們需要根據(jù) .proto 服務(wù)定義生成 gRPC 客戶端和服務(wù)器接口。我們使用帶有特殊 gRPC Go 插件的 protocol buffers 編譯器 protoc 來(lái)執(zhí)行此操作。
在 examples/route_guide 目錄中,運(yùn)行以下命令:
$ protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ routeguide/route_guide.proto運(yùn)行此命令將在 routeguide 目錄中生成以下文件:
route_guide.pb.go,其中包含用于填充,序列化和檢索請(qǐng)求和響應(yīng)消息類型的所有 protocol buffers 代碼。
route_guide_grpc.pb.go,其中包含以下內(nèi)容:
客戶端使用 RouteGuide 服務(wù)中定義的方法調(diào)用的接口類型(或存根)。
服務(wù)器要實(shí)現(xiàn)的接口類型,也具有 RouteGuide 服務(wù)中定義的方法。
04
創(chuàng)建服務(wù)器
首先,讓我們看一下如何創(chuàng)建 RouteGuide 服務(wù)器。
使我們的 RouteGuide 服務(wù)完成其工作包括兩個(gè)部分:
實(shí)施根據(jù)我們的服務(wù)定義生成的服務(wù)接口:完成我們服務(wù)的實(shí)際“工作”。
運(yùn)行 gRPC 服務(wù)器以監(jiān)聽來(lái)自客戶端的請(qǐng)求,并將其分派到正確的服務(wù)實(shí)現(xiàn)。
您可以在 server/server.go 中找到我們的示例 RouteGuide 服務(wù)器。讓我們仔細(xì)看看它是如何工作的。
實(shí)現(xiàn) RouteGuide
如您所見,我們的服務(wù)器具有一個(gè) routeGuideServer 結(jié)構(gòu)體類型,該結(jié)構(gòu)體類型實(shí)現(xiàn)了生成的 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 { ...}...簡(jiǎn)單的 RPC
routeGuideServer 實(shí)現(xiàn)我們所有的服務(wù)方法。首先,讓我們看一下最簡(jiǎn)單的類型 GetFeature,該類型僅從客戶端獲取一個(gè) Point,然后從其數(shù)據(jù)庫(kù)中的Feature 中返回相應(yīng)的 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{Location: point}, nil}該方法傳遞了 RPC 和客戶端的 Point protocol buffer 請(qǐng)求的上下文對(duì)象。它返回 Feature protocol buffer 對(duì)象以及響應(yīng)信息和錯(cuò)誤。在該方法中,我們使用適當(dāng)?shù)男畔⑻畛涔δ?#xff0c;然后將其返回并返回 nil 錯(cuò)誤,以告知 gRPC 我們已經(jīng)完成了對(duì) RPC 的處理,并且可以將 Feature 返回給客戶端。
服務(wù)器端流式 RPC
現(xiàn)在,讓我們看一下其中的流式 RPC。ListFeatures 是服務(wù)器端流式 RPC,因此我們需要將多個(gè) Feature 發(fā)送回客戶端。
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}如您所見,這次我們沒(méi)有獲得簡(jiǎn)單的請(qǐng)求和響應(yīng)對(duì)象,而是獲得了一個(gè)請(qǐng)求對(duì)象(客戶端要在其中找到 Feature 的 Rectangle)
和一個(gè)特殊的 RouteGuide_ListFeaturesServer 對(duì)象來(lái)編寫響應(yīng)。
在該方法中,我們填充了我們需要返回的所有 Feature 對(duì)象,并使用其 Send() 方法將它們寫入 RouteGuide_ListFeaturesServer。最后,就像在簡(jiǎn)單的 RPC 中一樣,我們返回 nil 錯(cuò)誤來(lái)告訴 gRPC 我們已經(jīng)完成了響應(yīng)的編寫。如果此調(diào)用中發(fā)生任何錯(cuò)誤,我們將返回非 nil 錯(cuò)誤;gRPC 層會(huì)將其轉(zhuǎn)換為適當(dāng)?shù)?RPC 狀態(tài),以在線上發(fā)送。
客戶端流式 RPC
現(xiàn)在,讓我們看一些更復(fù)雜的事情:客戶端流方法 RecordRoute,從客戶端獲取 Point 流,并返回一個(gè)包含行程信息的 RouteSummary。如您所見,這次方法完全沒(méi)有 request 參數(shù)。
相反,它獲得一個(gè)?
RouteGuide_RecordRouteServer 流,服務(wù)器可以使用該流來(lái)讀取和寫入消息-它可以使用 Recv()?方法接收客戶端消息,并使用SendAndClose()?方法返回其單個(gè)響應(yīng)。
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()?方法重復(fù)讀取客戶端對(duì)請(qǐng)求對(duì)象(在本例中為Point)的請(qǐng)求,直到?jīng)]有更多消息為止:服務(wù)器需要檢查從 Read()?返回的錯(cuò)誤。每個(gè) call。如果為 nil,則流仍然良好,并且可以繼續(xù)讀取;否則為 0。如果是 io.EOF,則消息流已結(jié)束,服務(wù)器可以返回其 RouteSummary。如果它具有其他值,我們將返回“原樣”錯(cuò)誤,以便 gRPC 層將其轉(zhuǎn)換為 RPC 狀態(tài)。
雙向流式 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) ... // look for notes to be sent to client for _, note := range s.routeNotes[key] { if err := stream.Send(note); err != nil { return err } } }}這次,我們獲得一個(gè) RouteGuide_RouteChatServer 流,就像在客戶端流示例中一樣,該流可用于讀取和寫入消息。但是,這次,當(dāng)客戶端仍在向其消息流中寫入消息時(shí),我們通過(guò)方法的流返回值。
此處的讀寫語(yǔ)法與我們的客戶端流式傳輸方法非常相似,不同之處在于服務(wù)器使用流的 Send()?方法而不是 SendAndClose()?,因?yàn)榉?wù)器正在寫多個(gè)響應(yīng)。盡管雙方總是會(huì)按照對(duì)方的寫入順序來(lái)獲取對(duì)方的消息,但是客戶端和服務(wù)器都可以以任意順序進(jìn)行讀取和寫入-流完全獨(dú)立地運(yùn)行。
啟動(dòng)服務(wù)器
一旦實(shí)現(xiàn)了所有方法,我們還需要啟動(dòng) gRPC 服務(wù)器,以便客戶端可以實(shí)際使用我們的服務(wù)。以下代碼段顯示了如何為 RouteGuide 服務(wù)執(zhí)行此操作:
flag.Parse()lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))if err != nil { log.Fatalf("failed to listen: %v", err)}var opts []grpc.ServerOption...grpcServer := grpc.NewServer(opts...)pb.RegisterRouteGuideServer(grpcServer, newServer())grpcServer.Serve(lis)構(gòu)建和啟動(dòng)服務(wù):
使用以下命令指定我們要用于監(jiān)聽客戶端請(qǐng)求的端口:
lis,err:= net.Listen(...)。
使用 grpc.NewServer(...)?創(chuàng)建 gRPC 服務(wù)器的實(shí)例。
在 gRPC 服務(wù)器上注冊(cè)我們的服務(wù)實(shí)現(xiàn)。
使用我們的端口詳細(xì)信息在服務(wù)器上調(diào)用 Serve()?進(jìn)行阻塞等待,直到進(jìn)程被殺死或調(diào)用 Stop()?為止。
05
創(chuàng)建客戶端
在本部分中,我們將研究為 RouteGuide 服務(wù)創(chuàng)建 Go 客戶端。
您可以在 grpc-go/examples/route_guide/client/client.go 中看到我們完整的示例客戶端代碼。
創(chuàng)建客戶端存根
要調(diào)用服務(wù)方法,我們首先需要?jiǎng)?chuàng)建一個(gè) gRPC 通道來(lái)與服務(wù)器通信。我們通過(guò)將服務(wù)器地址和端口號(hào)傳遞給 grpc.Dial()?來(lái)創(chuàng)建它,如下所示:
var opts []grpc.DialOption...conn, err := grpc.Dial(*serverAddr, opts...)if err != nil { ...}defer conn.Close()當(dāng)服務(wù)需要它們時(shí),可以使用 DialOptions 在 grpc.Dial 中設(shè)置身份驗(yàn)證憑據(jù)(例如TLS,GCE憑據(jù)或JWT憑據(jù))。RouteGuide 服務(wù)不需要任何憑據(jù)。
設(shè)置 gRPC 通道后,我們需要一個(gè)客戶端存根來(lái)執(zhí)行 RPC。我們使用從示例 .proto 文件生成的 pb 包提供的 NewRouteGuideClient 方法獲取它。
client?:=?pb.NewRouteGuideClient(conn)調(diào)用服務(wù)方法
現(xiàn)在,讓我們看看我們?nèi)绾握{(diào)用我們的服務(wù)方法。請(qǐng)注意,在 gRPC-Go 中,RPC 在阻塞/同步模式下運(yùn)行,這意味著 RPC 調(diào)用等待服務(wù)器響應(yīng),并且將返回響應(yīng)或錯(cuò)誤。
簡(jiǎn)單的 RPC
調(diào)用簡(jiǎn)單的 RPC GetFeature 幾乎與調(diào)用本地方法一樣簡(jiǎn)單。
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})if err != nil { ...}如您所見,我們?cè)谙惹矮@得的存根上調(diào)用該方法。在我們的方法參數(shù)中,我們創(chuàng)建并填充一個(gè)請(qǐng)求 protocol buffer 對(duì)象(在本例中為 Point)。我們還會(huì)傳遞一個(gè) context.Context 對(duì)象,該對(duì)象可讓我們?cè)诒匾獣r(shí)更改 RPC 的行為,例如 time-out/cancel 運(yùn)行中的 RPC。如果調(diào)用沒(méi)有返回錯(cuò)誤,那么我們可以從服務(wù)器的第一個(gè)返回值中讀取響應(yīng)信息。
log.Println(feature)服務(wù)器端流式 RPC
我們?cè)谶@里調(diào)用服務(wù)器端流方法 ListFeatures,該方法返回地理要素流。如果您已經(jīng)閱讀了創(chuàng)建服務(wù)器的內(nèi)容,那么其中的一些內(nèi)容可能看起來(lái)非常熟悉-流式 RPC 在兩側(cè)都以類似的方式實(shí)現(xiàn)。
rect := &pb.Rectangle{ ... } // initialize a pb.Rectanglestream, 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)}就像在簡(jiǎn)單的 RPC 中一樣,我們?yōu)樵摲椒▊鬟f一個(gè)上下文和一個(gè)請(qǐng)求。但是,我們沒(méi)有取回響應(yīng)對(duì)象,而是取回?
RouteGuide_ListFeaturesClient 的實(shí)例。
客戶端可以使用 RouteGuide_ListFeaturesClient 流讀取服務(wù)器的響應(yīng)。
我們使用?
RouteGuide_ListFeaturesClient 的 Recv()?方法重復(fù)讀取服務(wù)器對(duì)響應(yīng) protocol buffer 對(duì)象(在本例中為 Feature)的響應(yīng),直到?jīng)]有更多消息為止:客戶端需要檢查每次返回后從 Recv()?返回的錯(cuò)誤 err。如果為 nil,則流仍然良好,并且可以繼續(xù)讀取;如果是 io.EOF,則消息流已結(jié)束;否則,必須存在 RPC 錯(cuò)誤,該錯(cuò)誤會(huì)通過(guò) err 傳遞。
客戶端流式 RPC
客戶端流方法 RecordRoute 與服務(wù)器端方法相似,不同之處在于,我們僅向該方法傳遞上下文,并獲取回?
RouteGuide_RecordRouteClientClient 流,我們可以使用該流來(lái)寫入和讀取消息。
// Create a random number of random pointsr := rand.New(rand.NewSource(time.Now().UnixNano()))pointCount := int(r.Int31n(100)) + 2 // Traverse at least two pointsvar points []*pb.Pointfor i := 0; i < pointCount; i++ { points = append(points, randomPoint(r))}log.Printf("Traversing %d points.", len(points))stream, err := client.RecordRoute(context.Background())if err != nil { log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)}for _, point := range points { if err := stream.Send(point); err != nil { 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 具有一個(gè) Send()?方法,可用于將請(qǐng)求發(fā)送到服務(wù)器。使用 Send()?完成將客戶的請(qǐng)求寫入流中后,我們需要在流上調(diào)用 CloseAndRecv()?,以使 gRPC 知道我們已完成寫入并期望收到響應(yīng)。我們從 CloseAndRecv()?返回的錯(cuò)誤中獲取 RPC 狀態(tài)。如果狀態(tài)為 nil,則 CloseAndRecv()?的第一個(gè)返回值將是有效的服務(wù)器響應(yīng)。
雙向流式 RPC
最后,讓我們看一下雙向流式 RPC RouteChat()?。
與 RecordRoute 一樣,我們只向方法傳遞一個(gè)上下文對(duì)象,然后獲取可用于寫入和讀取消息的流。但是,這一次我們?cè)诜?wù)器仍將消息寫入消息流的同時(shí),我們還通過(guò)方法的流返回值。
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()除了在完成調(diào)用后使用流的 CloseSend()?方法外,此處的讀寫語(yǔ)法與我們的客戶端流方法非常相似。盡管雙方總是會(huì)按照對(duì)方的寫入順序來(lái)獲取對(duì)方的消息,但是客戶端和服務(wù)器都可以以任意順序進(jìn)行讀取和寫入-流完全獨(dú)立地運(yùn)行。
06
運(yùn)行程序
從 examples/route_guide 目錄執(zhí)行以下命令:
運(yùn)行服務(wù)器:
$ go run server/server.go從另一個(gè)終端,運(yùn)行客戶端:
$ go run client/client.go輸出內(nèi)容:
Getting feature for point (409146138, -746188906)name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<409146138 longitude:-746188906>409146138>Getting feature for point (0, 0)location:<>Looking for features within lo:<400000000 longitude:-750000000> hi:<420000000 longitude:-730000000>420000000>400000000>name:"Patriots Path, Mendham, NJ 07945, USA" location:<407838351 longitude:-746143763>407838351>...name:"3 Hasta Way, Newton, NJ 07860, USA" location:<410248224 longitude:-747127767>410248224>Traversing 56 points.Route summary: point_count:56 distance:497013163Got 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)注意:
我們已從本頁(yè)顯示的客戶端和服務(wù)器跟蹤輸出中省略了時(shí)間戳。
07
總結(jié)
本文開篇先介紹了為什么要使用 gRPC,接著簡(jiǎn)述了使用 gRPC 需要做的準(zhǔn)備工作,然后通過(guò) gRPC 官方 Go 示例代碼介紹了如何在 .proto 文件中定義服務(wù),如何使用 protoc 編譯器生成客戶端和服務(wù)器代碼,如何根據(jù) protoc 編譯器生成的客戶端和服務(wù)器代碼創(chuàng)建服務(wù)器和客戶端的 4 種形式。
參考資料:
https://grpc.io/docs/languages/go/basics/
總結(jié)
以上是生活随笔為你收集整理的go语言入门经典_Go 语言中的 gRPC 基础入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ae万能弹性表达式_AE脚本精品表达式合
- 下一篇: 用python读取图像_Python读取