開始使用 gRPC-Go

1. 簡介

在本程式碼研究室中,您將使用 gRPC-Go 建立用戶端和伺服器,做為以 Go 撰寫的路線對應應用程式基礎。

完成本教學課程後,您將擁有一個用戶端,可使用 gRPC 連線至遠端伺服器,取得地圖上特定座標位置的名稱或郵寄地址。完整的應用程式可能會使用這種用戶端/伺服器設計,列舉或摘要說明路線上的興趣點。

服務定義於 Protocol Buffers 檔案中,用於產生用戶端和伺服器的樣板程式碼,以便彼此通訊,節省您實作該功能的時間和精力。

這段產生的程式碼不僅會處理伺服器與用戶端之間複雜的通訊,也會處理資料序列化和還原序列化。

課程內容

  • 如何使用通訊協定緩衝區定義服務 API。
  • 如何使用自動程式碼生成功能,從通訊協定緩衝區定義建構以 gRPC 為基礎的用戶端和伺服器。
  • 瞭解如何透過 gRPC 進行用戶端與伺服器之間的通訊。

這個程式碼研究室適合剛接觸 gRPC 的 Go 開發人員、想複習 gRPC 的使用者,以及有興趣建構分散式系統的任何人。不需要有 gRPC 相關經驗。

2. 事前準備

必要條件

請確認已安裝下列項目:

  • Go 工具鍊 1.24.5 以上版本。如需安裝操作說明,請參閱 Go 的入門指南
  • 通訊協定緩衝區編譯器 protoc 3.27.1 以上版本。如需安裝操作說明,請參閱編譯器的安裝指南
  • Go 和 gRPC 適用的通訊協定緩衝區編譯器外掛程式。如要安裝這些外掛程式,請執行下列指令:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

更新 PATH 變數,讓通訊協定緩衝區編譯器可以找到外掛程式:

export PATH="$PATH:$(go env GOPATH)/bin"

取得程式碼

為避免您必須從頭開始,本程式碼研究室提供應用程式原始碼的架構,供您完成。下列步驟將說明如何完成應用程式,包括使用通訊協定緩衝區編譯器外掛程式產生樣板 gRPC 程式碼。

GitHub 將這個原始碼下載為 .ZIP 封存檔,然後解壓縮內容。

或者,如果您想略過輸入實作內容,也可以在 GitHub 上取得完整的原始碼

3. 定義服務

首先,請使用通訊協定緩衝區定義應用程式的 gRPC 服務、RPC 方法,以及要求和回應訊息類型。你的服務將提供:

  • 伺服器實作且用戶端呼叫的 RPC 方法,稱為 GetFeature
  • 使用 GetFeature 方法時,用戶端和伺服器之間交換的資料結構為 PointFeature 訊息類型。用戶端會在傳送至伺服器的 GetFeature 要求中,以 Point 形式提供地圖座標,而伺服器會回覆相應的 Feature,說明這些座標所指的位置。

這個 RPC 方法及其訊息型別都會在提供的原始碼 routeguide/route_guide.proto 檔案中定義。

通訊協定緩衝區通常稱為 protobuf。如要進一步瞭解 gRPC 術語,請參閱 gRPC 的「核心概念、架構和生命週期」。

訊息類型

在原始碼的 routeguide/route_guide.proto 檔案中,請先定義 Point 訊息型別。Point 代表地圖上的經緯度座標組合。在本程式碼研究室中,請使用整數做為座標:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

數字 12message 結構中每個欄位的專屬 ID 編號。

接著,定義 Feature 訊息類型。Feature 會使用 string 欄位,指定 Point 所指定位置的名稱或郵寄地址:

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

服務方法

route_guide.proto 檔案具有名為 RouteGuideservice 結構,可定義應用程式服務提供的一或多個方法。

RouteGuide 定義中新增 rpc 方法 GetFeature。如先前所述,這個方法會根據一組指定座標查閱地點名稱或地址,因此請讓 GetFeature 針對指定 Point 傳回 Feature

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

這是 unary RPC 方法:簡單的 RPC,用戶端會將要求傳送至伺服器,並等待伺服器傳回回應,就像呼叫本機函式一樣。

4. 產生用戶端和伺服器程式碼

接著,使用通訊協定緩衝區編譯器,從 .proto 檔案產生用戶端和伺服器的樣板 gRPC 程式碼。在 routeguide 目錄中執行:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

這個指令會產生下列檔案:

  • route_guide.pb.go,其中包含建立應用程式訊息類型及存取其資料的函式。
  • route_guide_grpc.pb.go,其中包含用戶端用來呼叫服務遠端 gRPC 方法的函式,以及伺服器用來提供該遠端服務的函式。

接著,我們會在伺服器端實作 GetFeature 方法,以便在用戶端傳送要求時,伺服器可以回覆答案。

5. 實作服務

伺服器端的 GetFeature 函式會完成主要工作:從用戶端接收 Point 訊息,並從已知地點清單中找出對應的位置資訊,然後以 Feature 訊息的形式傳回。以下是 server/server.go 中的函式實作方式:

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 呼叫的 Context 物件,以及來自該用戶端要求的 Point 通訊協定緩衝區物件。函式會傳回所查閱位置的 Feature 通訊協定緩衝區物件,並視需要傳回 error

在這個方法中,請使用指定 Point 的適當資訊填入 Feature 物件,然後連同 nil 錯誤一併 return,告知 gRPC 您已完成處理 RPC,且 Feature 物件可以傳回給用戶端。

GetFeature 方法需要建立及註冊 routeGuideServer 物件,這樣一來,用戶端的位置查詢要求就能傳送至該函式。請在 main() 中執行這項操作:

func main() {
  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...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

以下是 main() 的運作方式:

  1. 使用 lis, err := net.Listen(...) 指定要用來監聽遠端用戶端要求的 TCP 通訊埠。根據預設,應用程式會使用變數 port 指定的 TCP 通訊埠 50051,或在執行伺服器時,透過指令列傳遞 --port 切換。如果無法開啟 TCP 連接埠,應用程式會以嚴重錯誤結束。
  2. 使用 grpc.NewServer(...) 建立 gRPC 伺服器的執行個體,並將此執行個體命名為 grpcServer
  3. 建立指向 routeGuideServer 的指標,這是代表應用程式 API 服務的結構,並將指標命名為 s.
  4. 使用 s.loadFeatures() 填入陣列 s.savedFeatures,其中包含可透過 GetFeature 查詢的地點。
  5. 向 gRPC 伺服器註冊 API 服務,以便將對 GetFeature 的 RPC 呼叫,路由至適當的函式。
  6. 在伺服器上使用我們的連接埠詳細資料呼叫 Serve(),以執行用戶端要求的封鎖等待作業;這項作業會持續執行,直到程序終止或呼叫 Stop() 為止。

函式 loadFeatures() 會從 server/testdata.go 取得座標到位置的對應。

6. 建立用戶端

現在編輯 client/client.go,您將在此實作用戶端程式碼。

如要呼叫遠端服務的方法,我們必須先建立 gRPC 通道,與伺服器通訊。我們將伺服器的目標 URI 字串 (在本例中,這只是位址和連接埠號碼) 傳遞至用戶端 main() 函式中的 grpc.NewClient(),藉此建立這個項目,如下所示:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

伺服器的位址 (由變數 serverAddr 定義) 預設為 localhost:50051,執行用戶端時,可透過指令列的 --addr 參數覆寫。

如果用戶端需要連線至需要驗證憑證的服務 (例如 TLS 或 JWT 憑證),用戶端可以將 DialOptions 物件做為參數傳遞至 grpc.NewClient,其中包含必要憑證。RouteGuide 服務不需要任何憑證。

設定 gRPC 通道後,我們需要用戶端虛設常式,才能透過 Go 函式呼叫執行 RPC。我們使用從應用程式 .proto 檔案產生的 route_guide_grpc.pb.go 檔案提供的 NewRouteGuideClient 方法,取得該存根

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

呼叫服務方法

在 gRPC-Go 中,RPC 會以封鎖/同步模式運作,也就是說,RPC 呼叫會等待伺服器回應,並傳回回應或錯誤。

簡單的 RPC

呼叫簡單的 RPC GetFeature 幾乎與呼叫本機方法一樣簡單,在本例中為 client.GetFeature

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

用戶端會呼叫先前建立的 Stub 上的方法。對於方法參數,用戶端會建立並填入 Point 要求通訊協定緩衝區物件。您也會傳遞 context.Context 物件,讓我們視需要變更 RPC 的行為,例如定義呼叫的時間限制或取消進行中的 RPC。如果呼叫未傳回錯誤,用戶端可以從第一個傳回值讀取伺服器的回應資訊:

log.Println(feature)

總而言之,用戶端的 main() 函式應如下所示:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. 立即試用

在應用程式的工作目錄中執行下列指令,確認伺服器和用戶端可正常運作:

  1. 在一個終端機中執行伺服器:
cd server
go run .
  1. 從另一個終端機執行用戶端:
cd client
go run .

您會看到類似以下的輸出內容,為求清楚起見,時間戳記已省略:

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:<>

8. 後續步驟