发送元数据

在 gRPC 中,除了使用上下文进行请求取消和设置超时外,还可以通过上下文传递元数据。元数据可以是 HTTP 头部或 HTTP 尾部,它们都是键值对的列表,通常用于很多用途,例如传递身份验证令牌、数字签名、数据完整性等。在本节中,我们将重点介绍如何通过头部传递元数据。尾部是发送在消息之后的头部,通常开发人员使用较少,但 gRPC 在实现流接口时会使用它们。如果你对尾部感兴趣,可以查看 grpc.SetTrailer 函数( SetTrailer )。

我们的用例是向 UpdateTasks RPC 接口传递一个身份验证令牌,服务器验证令牌后,根据验证结果决定是更新任务还是返回 Unauthenticated 错误。当然,我们并不会涉及如何生成身份验证令牌的问题,假设正确的令牌是 authd,其它所有令牌都会被视为不正确。

服务器接收数据时可以从上下文中提取元数据,因此我们将使用 gRPC 中 metadata 包的 FromIncomingContext 函数来提取元数据。该函数返回一个元数据的映射,并告诉我们是否成功提取到了元数据。在 UpdateTasks 中,我们可以这样做:

func (s *server) UpdateTasks(stream pb.TodoService_UpdateTasksServer) error {
    ctx := stream.Context()
    md, _ := metadata.FromIncomingContext(ctx)
    // ...
}

现在,我们可以检查是否传递了身份验证令牌。为此,我们使用 Go 的映射(map)来访问 auth_token 元素。如果存在该键,它会返回值和一个布尔值,表示该键是否存在于映射中。如果存在,我们会检查它是否只有一个值,并且这个值是否为 "authd"。如果不是,我们返回一个 Unauthenticated 错误:

func (s *server) UpdateTasks(stream pb.TodoService_UpdateTasksServer) error {
    ctx := stream.Context()
    md, _ := metadata.FromIncomingContext(ctx)
    if t, ok := md["auth_token"]; ok {
        switch {
        case len(t) != 1:
            return status.Errorf(
                codes.InvalidArgument,
                "auth_token should contain only 1 value",
            )
        case t[0] != "authd":
            return status.Errorf(
                codes.Unauthenticated,
                "incorrect auth_token",
            )
        }
    } else {
        return status.Errorf(
            codes.Unauthenticated,
            "failed to get auth_token",
        )
    }
    // ...
}

这就是服务器端的全部内容;如果没有元数据、如果有元数据但没有 auth_token、如果 auth_token 有多个值,或者如果 auth_token 的值不是 "authd",我们都会返回一个错误。

现在我们可以转到客户端,发送适当的请求头。这可以通过元数据包中的另一个函数 AppendToOutgoingContext 来完成。我们知道在调用端点之前,已经创建了一个上下文,因此我们只需要将 auth_token 添加到该上下文中。实现起来非常简单,如下所示:

func updateTasks(c pb.TodoServiceClient, reqs ...*pb.UpdateTasksRequest) {
    ctx := context.Background()
    ctx = metadata.AppendToOutgoingContext(ctx, "auth_token", "authd")
    stream, err := c.UpdateTasks(ctx)
    // ...
}

我们用新的上下文覆盖了之前创建的上下文,这样就添加了包含键值对的元数据。请注意,键值对是可以交替添加的,也就是说,我们可以像下面这样同时传递多个键值对:

metadata.AppendToOutgoingContext(ctx, K1, V1, K2, V2, ...)

其中 K 表示键,V 表示值。

现在我们可以运行服务器:

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

然后运行客户端:

$ go run ./client 0.0.0.0:50051

如果一切正常,客户端会发送请求并得到相应的响应。然而,如果你将 auth_token 的值设置为非 "authd",你应该看到以下错误信息:

unexpected error: rpc error: code = Unauthenticated desc = incorrect auth_token

如果你没有设置 auth_token 头部,你会看到以下错误信息:

unexpected error: rpc error: code = Unauthenticated desc = failed to get auth_token

如果你设置了多个 auth_token 的值,比如:

ctx = metadata.AppendToOutgoingContext(ctx, "auth_token", "authd", "auth_token", "authd")

你将看到以下错误信息:

unexpected error: rpc error: code = InvalidArgument desc = auth_token should contain only 1 value

通过本节,我们学习了如何从上下文中获取元数据,使用 metadata.FromIncomingContext 函数提取元数据,并处理可能出现的各种错误。同时,我们也学会了如何通过 metadata.AppendToOutgoingContext 函数将元数据从客户端发送到服务器。