服务器模板代码

我们的构建系统已经准备好了,现在我们可以专注于代码部分。但是,在开始之前,我们先来定义一下我们想要的目标。在这一节中,我们希望构建一个可重用的 gRPC 服务器模板,以便在后续章节甚至未来的项目中使用。为了实现这一点,我们希望避免以下几点:

我们可以通过不再关心生成的代码来解决这些问题。生成的代码仅用于测试我们的构建系统。然后,我们将默认使用不安全的连接进行测试。最后,我们将把 IP 地址作为程序的参数。

让我们一步步来实现这些目标:

  1. 添加 gRPC 依赖

    我们首先需要将 gRPC 依赖添加到 server/go.mod 文件中。因此,在 server 目录下,我们可以输入以下命令:

    $ go get google.golang.org/grpc
  2. 获取程序参数

    接下来,我们将获取传递给程序的第一个参数,并在没有传递参数时返回用法消息:

    args := os.Args[1:]
    if len(args) == 0 {
        log.Fatalln("usage: server [IP_ADDR]")
    }
    addr := args[0]
  3. 监听传入的连接

    然后,我们需要使用 Go 提供的 net.Listen 来监听传入的连接。该监听器在程序结束时需要关闭。这个关闭操作可能是在用户终止程序时,或者在服务器出现故障时。显然,如果在构建监听器时出现错误,我们希望程序失败并提示用户:

    lis, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatalf("failed to listen: %v\n", err)
    }
    defer func(lis net.Listener) {
        if err := lis.Close(); err != nil {
            log.Fatalf("unexpected error: %v", err)
        }
    }(lis)
    log.Printf("listening at %s\n", addr)
  4. 创建 gRPC 服务器

    现在,借助之前的配置,我们可以开始创建一个 grpc.Server。首先,我们需要定义一些连接选项。由于这是一个未来项目的模板,我们将保持选项为空。使用 grpc.ServerOption 数组,我们可以创建一个新的 gRPC 服务器。这个服务器将在后续步骤中用于注册端点。然后,我们需要在某个时刻关闭服务器,因此我们使用 defer 语句来处理。最后,我们调用 grpc.Server 上的 Serve 函数,传入监听器作为参数。如果发生错误,我们将其返回给客户端:

    opts := []grpc.ServerOption{}
    s := grpc.NewServer(opts...)
    // 注册端点
    defer s.Stop()
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v\n", err)
    }

    在完成上述步骤后,我们的 main 函数(server/main.go)将如下所示:

    package main
    
    import (
        "log"
        "net"
        "os"
    
        "google.golang.org/grpc"
    )
    
    func main() {
        args := os.Args[1:]
    
        if len(args) == 0 {
            log.Fatalln("usage: server [IP_ADDR]")
        }
    
        addr := args[0]
        lis, err := net.Listen("tcp", addr)
    
        if err != nil {
            log.Fatalf("failed to listen: %v\n", err)
        }
    
        defer func(lis net.Listener) {
            if err := lis.Close(); err != nil {
                log.Fatalf("unexpected error: %v", err)
            }
        }(lis)
    
        log.Printf("listening at %s\n", addr)
    
        opts := []grpc.ServerOption{}
        s := grpc.NewServer(opts...)
    
        // 注册端点
    
        defer s.Stop()
        if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v\n", err)
        }
    }

现在,我们可以通过在 server/main.go 上运行 go run 命令来启动服务器。我们可以通过使用 【Ctrl + C】 来终止执行:

$ go run server/main.go 0.0.0.0:50051
listening at 0.0.0.0:50051

要停止服务器,可以使用 Ctrl + C

这个模板现在可以作为我们后续章节和未来项目的基础。

Bazel

如果你想使用 Bazel,你需要进行一些额外的步骤。第一步是更新我们根目录下的 BUILD.bazel 文件。在这个文件中,我们将使用一个 Gazelle 命令,它将自动检测项目所需的所有依赖,并将它们输出到一个名为 deps.bzl 的文件中。执行 Gazelle 命令后,我们只需添加以下内容:

gazelle(
    name = "gazelle-update-repos",
    args = [
        "-from_file=go.work",
        "-to_macro=deps.bzl%go_dependencies",
        "-prune",
    ],
    command = "update-repos",
)

然后,我们可以运行以下命令:

$ bazel run //:gazelle-update-repos

该命令完成后,它将检测到服务器模块的所有依赖并创建一个 deps.bzl 文件,并将其链接到我们的 WORKSPACE.bazel 文件中。你应该在 WORKSPACE.bazel 文件中看到以下内容:

load("//:deps.bzl", "go_dependencies")

# gazelle:repository_macro deps.bzl%go_dependencies
go_dependencies()

接下来,我们可以重新运行 Gazelle 命令,以确保它为我们的服务器创建了 BUILD.bazel 文件。我们运行以下命令:

$ bazel run //:gazelle

然后,server 目录中将生成 BUILD.bazel 文件。需要注意的是,在这个文件中,我们可以看到 Bazel 将 gRPC 连接到 server_lib。我们应该看到类似这样的内容:

go_library(
    name = "server_lib",
    srcs = ["main.go"],
    deps = [
        "@org_golang_google_grpc//:go_default_library",
    ],
    #...
)

现在,我们可以像使用 go run 命令一样运行我们的服务器:

此命令将拉取 Protobuf 并进行构建。最近版本的 Protobuf 需要使用 C++14 或更新版本进行构建。你可以在名为 .bazelrc 的文件中告诉 Bazel 自动指定构建 Protobuf 时使用的 C++ 版本。为了使本章与版本无关,建议你检查 GitHub 仓库中 chapter4.bazelrc 文件。你可以将该文件复制到你的项目文件夹中。

$ bazel run //server:server 0.0.0.0:50051
listening at 0.0.0.0:50051

到此为止,我们已经完成了服务器的设置。这个简单的模板将使我们能够在接下来的章节中轻松地创建新的服务器。它正在指定的端口上监听并等待请求。接下来,为了简化请求的发送,我们将为客户端创建一个模板。