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
方法时,客户端和服务器之间交换的数据结构消息类型Point
和Feature
。客户端在其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;
}
数字 1
和 2
是 message
结构中每个字段的唯一 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
文件具有名为 RouteGuide
的 service
结构,用于定义应用服务提供的一个或多个方法。
在 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()
中发生的情况,分步说明如下:
- 使用
lis, err := net.Listen(...)
指定用于侦听远程客户端请求的 TCP 端口。默认情况下,应用使用变量port
指定的 TCP 端口50051
,或者在运行服务器时通过命令行传递--port
开关。如果无法打开 TCP 端口,应用会因严重错误而结束。 - 使用
grpc.NewServer(...)
创建 gRPC 服务器的实例,并将此实例命名为grpcServer
。 - 创建指向
routeGuideServer
(表示应用的 API 服务的结构)的指针,并将该指针命名为s.
- 使用
s.loadFeatures()
填充数组s.savedFeatures
,其中包含可通过GetFeature
查找的地理位置。 - 向 gRPC 服务器注册 API 服务,以便将对
GetFeature
的 RPC 调用路由到相应的函数。 - 在服务器上调用
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. 试试看
在应用的工作目录中执行以下命令,确认服务器和客户端是否正常协作:
- 在一个终端中运行服务器:
cd server go run .
- 从另一个终端运行客户端:
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:<>