gRPC-Go 使用入门

1. 简介

在此 Codelab 中,您将使用 gRPC-Go 创建一个客户端和服务器,它们将构成使用 Go 编写的路线映射应用的基础。

在本教程结束时,您将拥有一个使用 gRPC 连接到远程服务器的客户端,以获取地图上特定坐标处的位置名称或邮寄地址。一个功能完善的应用可能会使用这种客户端-服务器设计来枚举或总结路线沿途的兴趣点。

该服务在 Protocol Buffers 文件中定义,该文件将用于为客户端和服务器生成样板代码,以便它们能够相互通信,从而节省您实现该功能的时间和精力。

生成的代码不仅能处理服务器与客户端之间复杂的通信,还能处理数据序列化和反序列化。

学习内容

  • 如何使用 Protocol Buffers 定义服务 API。
  • 如何使用自动代码生成功能基于 Protocol Buffers 定义构建基于 gRPC 的客户端和服务器。
  • 了解使用 gRPC 进行客户端-服务器通信。

此 Codelab 适合刚开始使用 gRPC 或希望复习 gRPC 的 Go 开发者,也适合有意构建分布式系统的任何人。无需具备 gRPC 经验。

2. 准备工作

前提条件

请确保您已安装以下各项:

  • Go 工具链版本 1.24.5 或更高版本。如需查看安装说明,请参阅 Go 的使用入门
  • Protocol Buffers 编译器 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 变量,以便 Protocol Buffer 编译器可以找到插件:

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

获取代码

为了避免您完全从头开始,本 Codelab 提供了一个应用源代码框架供您完成。以下步骤将展示如何完成应用,包括使用 Protocol Buffer 编译器插件生成样板 gRPC 代码。

从 GitHub 下载此源代码的 .ZIP 归档文件,然后解压缩其内容。

或者,如果您想跳过输入实现代码的步骤,可以在 GitHub 上找到完整的源代码

3. 定义服务

第一步是使用协议缓冲区定义应用的 gRPC 服务、RPC 方法以及请求和响应消息类型。您的服务将提供:

  • 一种名为 GetFeature 的 RPC 方法,由服务器实现并由客户端调用。
  • 使用 GetFeature 方法时,客户端和服务器之间交换的数据结构消息类型 PointFeature。客户端在其 GetFeature 请求中向服务器提供地图坐标作为 Point,服务器则会回复相应的 Feature,其中描述了位于这些坐标处的任何内容。

此 RPC 方法及其消息类型都将在所提供源代码的 routeguide/route_guide.proto 文件中定义。

协议缓冲区通常称为 protobuf。如需详细了解 gRPC 术语,请参阅 gRPC 的核心概念、架构和生命周期

消息类型

在源代码的 routeguide/route_guide.proto 文件中,首先定义 Point 消息类型。Point 表示地图上的纬度-经度坐标对。在此 Codelab 中,请使用整数作为坐标:

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) {}
}

这是一元 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 方法获取该 stub

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)
}

客户端调用之前创建的桩上的方法。对于该方法的参数,客户端会创建并填充 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. 后续步骤