用Golang構建gRPC服務

本教程提供了Go使用gRPC的基礎教程git

在教程中你將會學到如何:github

  • .proto文件中定義一個服務。
  • 使用protocol buffer編譯器生成客戶端和服務端代碼。
  • 使用gRPC的Go API爲你的服務寫一個客戶端和服務器。

繼續以前,請確保你已經對gRPC概念有所瞭解,而且熟悉protocol buffer。須要注意的是教程中的示例使用的是proto3版本的protocol buffer:你能夠在Protobuf語言指南Protobuf生成Go代碼指南中瞭解到更多相關知識。golang

爲何使用gRPC

咱們的示例是一個簡單的路線圖應用,客戶端能夠獲取路線特徵信息、建立他們的路線摘要,還能夠與服務器或者其餘客戶端交換好比交通狀態更新這樣的路線信息。數據庫

藉助gRPC,咱們能夠在.proto文件中定義咱們的服務,並以gRPC支持的任何語言來實現客戶端和服務器,客戶端和服務器又能夠在從服務器到你本身的平板電腦的各類環境中運行-gRPC還會爲你解決全部不一樣語言和環境之間通訊的複雜性。咱們還得到了使用protocol buffer的全部優勢,包括有效的序列化(速度和體積兩方面都比JSON更有效率),簡單的IDL(接口定義語言)和輕鬆的接口更新。服務器

安裝

安裝grpc包

首先須要安裝gRPC golang版本的軟件包,同時官方軟件包的examples目錄裏就包含了教程中示例路線圖應用的代碼。app

$ go get google.golang.org/grpc
複製代碼

而後切換到``grpc-go/examples/route_guide:目錄:dom

$ 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 下載並解壓文件。tcp

更新PATH系統變量,或者確保protoc放在了PATH包含的目錄中了。ide

  • 安裝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服務中定義的全部方法。

建立gRPC服務端

首先讓咱們看一下怎麼建立RouteGuide服務器。有兩種方法來讓咱們的RouteGuide服務工做:

  • 實現咱們從服務定義生成的服務接口:作服務實際要作的事情。
  • 運行一個gRPC服務器監聽客戶端的請求而後把請求派發給正確的服務實現。

你能夠在剛纔安裝的gPRC包的grpc-go/examples/route_guide/server/server.go找到咱們示例中RouteGuide`服務的實現代碼。下面讓咱們看看他是怎麼工做的。

實現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 {
        ...
}
...
複製代碼

普通PRC

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

如今,讓咱們看一下服務方法中的一個流式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
}
複製代碼

如你所見,此次咱們沒有得到簡單的請求和響應對象,而是得到了一個請求對象(客戶端要在其中查找FeatureRectangle)和一個特殊的RouteGuide_ListFeaturesServer對象來寫入響應。

在該方法中,咱們填充了須要返回的全部Feature對象,並使用Send()方法將它們寫入RouteGuide_ListFeaturesServer。最後,就像在簡單的RPC中同樣,咱們返回nil錯誤來告訴gRPC咱們已經完成了響應的寫入。若是此調用中發生任何錯誤,咱們將返回非nil錯誤; gRPC層會將其轉換爲適當的RPC狀態,以在線上發送。

客戶端流式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_RecordRouteServerRecv()方法不停地讀取客戶端的請求到一個請求對象中(在本例中爲Point),直到沒有更多消息爲止:服務器須要要在每次調用後檢查從Recv()返回的錯誤。若是爲nil,則流仍然良好,而且能夠繼續讀取;若是是io.EOF,則表示消息流已結束,服務器能夠返回其RouteSummary。若是錯誤爲其餘值,咱們將返回錯誤「原樣」,以便gRPC層將其轉換爲RPC狀態。

雙向流式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

調用普通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)。若是調用沒有返回錯誤,則咱們能夠從第一個返回值中讀取服務器的響應信息。

服務端流式RPC

這裏咱們會調用服務端流式方法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_ListFeaturesClientRecv()方法不停地將服務器的響應讀入到一個protocol buffer響應對象中(本例中的Feature對象),直到沒有更多消息爲止:客戶端須要在每次調用後檢查從Recv()返回的錯誤err。若是爲nil,則流仍然良好,而且能夠繼續讀取;若是是io.EOF,則消息流已結束;不然就是必定RPC錯誤,該錯誤會經過err傳遞給調用程序。

客戶端流式RPC

客戶端流方法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

最後,讓咱們看一下雙向流式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
複製代碼
相關文章
相關標籤/搜索