接下来,我们在实现 GRPC 客户端服务发现 的时候,首先需要编写一个 proto 文件。这个 proto 文件是定义服务的基础,只有创建好之后,GRPC 才会自动生成对应的 服务端和客户端代码。我们后续的开发工作,都需要依赖这些自动生成的代码来完成。
在开始之前,我们先来创建一个 proto 文件。以用户注册功能为例,我们将其命名为 userProtocol.proto
。创建完成后,我们需要将文件内容拷贝到指定目录。具体来说,我们将其放置在 UZ
下的 PB
目录中。这个目录是我们指定的生成代码的存放位置,同时,生成的 Go 代码的包名(package)也命名为 PB
。
我们的服务接口要求的参数包括 account
、password
等。这些参数来源于接口文档中定义的请求参数。接口文档中还规定了返回值,包括一个 token
和一个服务地址(server address
)。在注册完成后,我们通常会得到一个用户 ID(uid
),然后根据这个用户 ID 生成 token
。至于服务地址,为了简单起见,我们暂时直接返回一个固定的地址,后续再逐步实现负载均衡等功能。
在用户服务的实现中,我们只需要返回 uid
,以便网关根据这个 uid
生成对应的 token
。因此,我们只需要关注这两个关键的参数。
接下来,我们定义一个服务(service
)。有了 proto 文件之后,我们需要生成对应的代码。生成代码时,我们可以通过编写一个脚本来简化操作。由于我使用的是 Windows 系统,因此我编写了一个 .bat
脚本。这个脚本本质上是一个命令行工具,用于调用 protoc
命令来生成代码。
在开始之前,大家一定要确保安装了 protoc
(Protocol Buffers 的编译器),并且将其配置到系统的 PATH 路径中。配置完成后,我们就可以通过命令行调用 protoc
来生成代码了。
package rpc
import (
"common/config"
"common/logs"
"context"
"core/discovery"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
"user/pb"
)
var (
UserClient pb.UserServiceClient
)
func Init() {
r := discovery.NewResolver(config.Conf.Etcd)
resolver.Register(r)
domain := config.Conf.Domain["user"]
initClient(r.Scheme(), domain.Name, domain.LoadBalance, &UserClient)
}
func initClient(scheme, name string, loadBalance bool, client interface{}) {
addr := fmt.Sprintf("%s:///%s", scheme, name)
opts := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials())}
if loadBalance {
opts = append(opts, grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy": "%s"}`, "round_robin")))
}
conn, err := grpc.DialContext(context.TODO(), addr, opts...)
if err != nil {
logs.Fatal("rpc connect etcd error: %v", err)
}
switch c := client.(type) {
case *pb.UserServiceClient:
*c = pb.NewUserServiceClient(conn)
default:
logs.Fatal("unsupported client type")
}
}
生成代码时,我们需要指定输出目录,这里是 PB
目录。同时,我们还需要将 GRPC 相关的代码也生成到这个目录中。具体操作是根据 userProtocol.proto
文件来生成对应的代码。生成完成后,我们可以通过运行脚本来完成整个过程。运行完成后,关闭脚本,生成的代码就会出现在指定目录中。
生成的代码中包含了服务端和客户端的相关代码,这些代码是我们后续实现功能的基础。我们需要在这些代码的基础上,实现具体的业务逻辑。
接下来,我们将服务注册到 GRPC Server 中。在注册时,我们需要调用 registerUserServiceServer
方法,将我们实现的服务注册到 GRPC Server 中。注册完成后,我们的服务就可以被客户端调用了。
为了实现服务,我们首先需要创建一个服务类。通常情况下,我们将服务类放在内部目录(internal
)中,以避免被其他模块直接引用。这样可以更好地封装我们的业务逻辑。
以用户服务为例,我们创建一个 userService
类,并在其中实现具体的业务逻辑。例如,我们实现一个 CreateAccount
方法,用于处理用户注册的逻辑。在这个方法中,我们需要与数据库进行交互,因此需要引入数据库连接管理器。
在实现业务逻辑时,我们需要注意,我们的服务类需要实现 proto 文件中定义的接口。这意味着我们需要按照接口中定义的方法和参数来实现服务。
实现完成后,我们可以在服务类中打印一些日志信息,以便在调用时能够看到服务是否被正确调用。例如,我们可以在 CreateAccount
方法中打印一条日志,表示注册方法被调用。
接下来,我们需要在网关中调用这个服务。网关需要通过客户端来调用服务。客户端是通过 GRPC 的 PB
文件生成的。我们需要在网关中建立一个 GRPC 客户端,用于连接服务端。
为了建立客户端,我们需要进行初始化操作。这个初始化操作可以放在一个通用的模块中,例如 common
模块,因为其他模块也可能需要使用 GRPC 客户端。在初始化时,我们需要指定服务的地址,这个地址可以通过 ETCD 获取。
在网关中,我们可以通过调用服务端的接口来获取服务地址。然后,我们通过这个地址建立 GRPC 连接。连接成功后,我们就可以通过客户端调用服务端的方法了。
例如,我们可以在网关中调用 CreateAccount
方法,并传递相应的参数。调用成功后,服务端会返回一个 uid
,我们根据这个 uid
生成对应的 token
,并返回给客户端。
整个流程中,我们需要关注的关键点包括:
-
proto 文件的编写:定义服务接口和参数。
-
代码生成:通过
protoc
工具生成服务端和客户端代码。 -
服务注册:将服务注册到 GRPC Server 中。
-
业务逻辑实现:在服务类中实现具体的业务逻辑。
-
客户端调用:在网关中通过客户端调用服务端的方法。
接下来,我们需要编写一个 ETCD 解析器,用于在 GRPC 连接时从 ETCD 中获取服务地址。这个解析器是整个服务发现机制的核心部分,它确保客户端能够正确地找到服务端的地址。