gRPC 的元数据


简介


gRPC 应用程序通常会通过 gRPC 服务和消费者之间的 RPC 来共享信息,在大多数场景中,与服务业务逻辑和消费者直接相关的信息会作为远程方法调用参数的一部分。

但在某些场景中,因为预期共享的关于 RPC 的信息可能与 RPC 业务上下文并没有关联,所以它们不应该作为 RPC 参数的一部分。在这样的场景中,可以使用 gRPC 元数据(gRPC metadata),元数据可以在 gRPC 服务或 gRPC 客户端发送和接收,如下图所示:

在客户端或服务器端创建的元数据,可以通过 gRPC 头信息在客户端应用程序和服务器端应用程序之间进行交换。元数据的构造遵循 键(字符串)–值 对的形式,其中键是 string 类型,值通常是 []string 类型,但也可以是二进制数据,metadata 类型定义如下:

type MD map[string][]string

元数据可以像普通 map 一样读取,但这个 map 的值类型是 []string 类型,因此用户可以使用一个键附加多个值。

metadata 中的键是大小写不敏感的,由字母、数字和特殊字符 -_. 组成并且不能以 grpc- 开头(gRPC 保留自用),二进制值的键名必须以 -bin 结尾。

元数据对 gRPC 本身是不可见的,通常在应用程序代码或中间件中处理元数据,不需要在 .proto 文件中指定元数据。

访问元数据取决于具体使用的编程语言, 在 Go 语言中使用 google.golang.org/grpc/metadata 库来操作 metadata。


创建和检索元数据


创建新的 metadata

在 gRPC 应用程序中,存在以下两种创建元数据的方式。

  • 使用 metadata.New() 函数基于 map[string]string 创建元数据:
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
  • 使用 metadata.Pairs() 函数创建元数据对(具有相同键的值将合并到一个列表中):
md := metadata.Pairs(
    	"key1", "val1",
    	"key1", "val1-2", // "key1" 的值为 []string{"val1", "val1-2"}
    	"key2", "val2",
)

所有的键将自动转换为小写,因此 “kEy1” 和 “Key1” 将是相同的键,它们的值将合并到相同的列表中,这种情况适用于 New() 函数和 Pair() 函数。


元数据中存储二进制数据

在元数据中,键始终是字符串,值可以是字符串或二进制数据(即二进制数据也可以设置为元数据值)。若要在元数据中存储二进制数据值,只需在密钥中添加 “-bin” 后缀,以元数据值形式所设置的二进制数据在发送之前会进行 base64 编码,在传输之后,则会进行解码。例如以下代码在创建元数据时,将对带有 “-bin” 后缀键的值进行编码:

md := metadata.Pairs(
    	"key", "string value",
    	"key-bin", string([]byte{96, 102}), // 二进制数据在发送前会进行(base64) 编码,收到后会进行解码
)

读取元数据

在客户端或服务器端读取元数据,则可以通过传入的 RPC 上下文以 metadata.FromIncomingContext(ctx) 函数来实现,它会返回 Go 语言的元数据 map ,具体的程序代码如下:

// 从"md" 元数据 map 中读取所需的元数据
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    	md, ok := metadata.FromIncomingContext(ctx)
    	// do something with metadata
}

发送和接收元数据


客户端

(1)发送元数据

在客户端,要发送元数据到 gRPC 服务,可以创建元数据并将其设置到 RPC 上下文中,在 Go 语言中有以下两种方法可以将元数据发送到服务端:

  • 使用 NewOutgoingContext() 创建带有新元数据的上下文。当使用 NewOutgoingContext 时会替换掉上下文中所有已有的元数据;

  • 使用 AppendToOutgoingContext() 将元数据附加到已有的上下文中。当使用 AppendToOutgoingContext() 将 kv 对附加到 context 中,无论 context 中是否已经有元数据都可以使用这个方法,如果先前没有元数据,则添加元数据;如果 context 中已经存在元数据,则将 kv 对合并进去。

实现的发送参考以下的程序代码:

// 创建元数据
md := metadata.Pairs(
		"timestamp", time.Now().Format(time.StampNano),
		"kn", "vn",
) 
// 基于新的元数据创建新的上下文
mdCtx := metadata.NewOutgoingContext(context.Background(), md) 
// 添加一些 metadata 到 context (e.g. in an interceptor)
send, _ := metadata.FromOutgoingContext(mdctx)
// 在现有的上下文中附加更多的元数据
ctxA := metadata.AppendToOutgoingContext(mdCtx, "k1", "v1", "k1", "v2", "k2", "v3") 

// 发送一元 RPC,一元 RPC 使用带有元数据的新上下文
response, err := client.SomeRPC(ctxA, someRequest) 
// 发送流 RPC,相同的上下文也可用于流 RPC
stream, err := client.SomeStreamingRPC(ctxA) 

在创建完带有元数据的上下文后,它就可以用于一元 RPC 或流 RPC 了。


(2)接收元数据

在客户端接收元数据的时候,需要将它们视为头信息(header)或 trailer ,trailer 可以用于服务器希望在处理请求后给客户端发送任何内容,例如在流式 RPC 中只有等所有结果都流到客户端后才能计算出负载信息,这时候就不能使用 headers(header 在数据之前,trailer 在数据之后)。

  • 对于一元 RPC ,调用 CallOption 中的 Header() 函数和 Trailer() 函数来获取 RPC 调用发送的 header 和 trailer ;

  • 对于流 RPC ,调用 ClientStream 中的 Header() 方法和 Trailer() 方法来获取 RPC 调用发送的 header 和 trailer 。

具体的使用参考以下程序代码:

// 用来存储 RPC 所返回的头信息和 trailer 的变量
var header, trailer metadata.MD 

// *****一元RPC*****
// 传递头信息和 trailer 引用来存储一元 RPC 所返回的值
r, err := client.SomeRPC( 
		ctx, 
		someRequest,
		grpc.Header(&header),
		grpc.Trailer(&trailer),
)

// 在这里处理头信息和 trailer map

// *****流RPC*****
stream, err := client.SomeStreamingRPC(ctx)

// 检索头信息,从流中获取头信息
header, err := stream.Header() 

// 检索 trailer,从流中获取 trailer,用于发送状态码和状态消息
trailer := stream.Trailer() 

// 在这里处理头信息和 trailer map

从对应的 RPC 操作获取值之后,就可以像一般的 map 那样对它们进行处理,进而处理所需的元数据.


服务端

(1)发送元数据

从服务器端发送元数据,可以根据元数据发送头信息或者设置 trailer ,在 Go 语言中有以下两种方式可以将元数据发送到客户端:

  • 对于一元 RPC ,可以调用 grpc 模块中的 SendHeader() 方法和 SetTrailer() 方法向客户端发送 header 和 trailer ,这两个方法将 context 作为第一个参数;

  • 对于流式 RPC ,可以调用 ServerStream 中的 SendHeader () 方法和 SetTrailer() 方法发送 header 和 trailer 。

具体的使用参考以下程序代码:

func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
		// 创建并发送头信息
		header := metadata.Pairs("header-key", "val") 	
		// 以头信息的形式发送元数据
		grpc.SendHeader(ctx, header) 

		// 创建并设置 trailer
		trailer := metadata.Pairs("trailer-key", "val") 
		// 和 trailer 一起发送元数据
		grpc.SetTrailer(ctx, trailer) 
}

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
		// 创建并发送头信息
		header := metadata.Pairs("header-key", "val") 
		// 在流中,以头信息的形式发送元数据
		stream.SendHeader(header) 

		// 创建并设置 trailer
		trailer := metadata.Pairs("trailer-key", "val") 
		// 和流的 trailer 一起发送元数据
		stream.SetTrailer(trailer) 
}

在一元 RPC 和流 RPC 这两种场景中,都可以通过 grpc.SendHeader() 方法来发送元数据,如果想将元数据作为 trailer 的一部分发送,则需要通过 grpc.SetTrailer() 方法或对应流的 SetTrailer() 方法,将元数据设置为上下文 trailer 中的一部分。


元数据操作程序示例


一元 RPC

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 metadata.proto 文件,具体的目录结构如下所示:

UnaryMetadata
├── client
│   └── proto
│       └── metadata.proto
└── server
    └── proto
        └── metadata.proto

metadata.proto 文件的具体内容如下所示:

syntax = "proto3";
option go_package="../proto";

package metadata;
// The greeting service definition.
service Greeter {
  		// Sends a greeting
  		rpc UnarySayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  		string name = 1;
}

// The response message containing the greetings
message HelloReply {
  		string message = 1;
}

(2)进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

UnaryMetadata
├── client
│   └── proto
│       ├── metadata_grpc.pb.go
│       ├── metadata.pb.go
│       └── metadata.proto
└── server
    └── proto
        ├── metadata_grpc.pb.go
        ├── metadata.pb.go
        └── metadata.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package main

import (
        "context"
        "fmt"
        pb "server/proto"
        "net"
        "time"
        "strconv"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
        "google.golang.org/grpc"
        "google.golang.org/grpc/metadata"
)


type server struct {
        pb.UnimplementedGreeterServer
}

// UnarySayHello 普通 RPC 调用服务端 metadata 操作
func (s *server) UnarySayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        // 通过 defer 中设置 trailer 
        defer func() {
                trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
                grpc.SetTrailer(ctx, trailer)
        }()

        // 从客户端请求上下文中读取 metadata
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
                return nil, status.Errorf(codes.DataLoss, "UnarySayHello: failed to get metadata")
        }
        if t, ok := md["token"]; ok {
                fmt.Printf("token from metadata:\n")
                if len(t) < 1 || t[0] != "admin" {
                        return nil, status.Error(codes.Unauthenticated, "认证失败!")
                }
        }

        // 创建和发送 header
        header := metadata.New(map[string]string{"location": "ChongQing"})
        grpc.SendHeader(ctx, header)

        fmt.Printf("Request received: %v, say hello...\n", in)

        return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
        // 监听本地的 8972 端口
        lis, err := net.Listen("tcp", ":8972")
        if err != nil {
                fmt.Printf("failed to listen: %v", err)
                return
        }
        s := grpc.NewServer()                  // 创建 gRPC 服务器
        pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务
        // 启动服务
        err = s.Serve(lis)
        if err != nil {
                fmt.Printf("failed to serve: %v", err)
                return
        }
}

(4)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package main

import (
        "context"
        "flag"
        "log"
        "google.golang.org/grpc/metadata"
        pb "client/proto"
        "fmt"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

const (
        defaultName = "cqupthao!"
)

var (
        addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
)

// unaryCallWithMetadata 普通RPC调用客户端metadata操作
func unaryCallWithMetadata(c pb.GreeterClient, name string) error {
        fmt.Println("--- UnarySayHello client---")
        // 创建metadata
        md := metadata.Pairs(
                "token", "admin",
                "request_id", "1234567",
        )
        // 基于metadata创建context.
        ctx := metadata.NewOutgoingContext(context.Background(), md)
        // RPC调用
        var header, trailer metadata.MD
        r, err := c.UnarySayHello(
                ctx,
                &pb.HelloRequest{Name: name},
                grpc.Header(&header),   // 接收服务端发来的header
                grpc.Trailer(&trailer), // 接收服务端发来的trailer
        )
        if err != nil {
                log.Printf("failed to call SayHello: %v", err)
                return err
        }
        // 从header中取location
        if t, ok := header["location"]; ok {
                fmt.Printf("location from header:\n")
                for i, e := range t {
                        fmt.Printf(" %d. %s\n", i, e)
                }
        } else {
                log.Printf("location expected but doesn't exist in header")
                return err
        }
   // 获取响应结果
        fmt.Printf("got response: %s\n", r.Message)
        // 从trailer中取timestamp
        if t, ok := trailer["timestamp"]; ok {
                fmt.Printf("timestamp from trailer:\n")
                for i, e := range t {
                        fmt.Printf(" %d. %s\n", i, e)
                }
        } else {
                log.Printf("timestamp expected but doesn't exist in trailer")
        }

        return nil
}

func main() {
        flag.Parse()
        // 连接到server端,此处禁用安全传输
        conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        err = unaryCallWithMetadata(c, defaultName)
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }

}

执行 Server 端和 Client 端的程序,服务端和客户端分别输出如下的结果:

// server
token from metadata:
Request received: name:"cqupthao!", say hello...

// client
--- UnarySayHello client---
location from header:
 0. ChongQing
got response: Hello cqupthao!
timestamp from trailer:
 0. 1677420120

若传递错误的 Token ,则输出如下的结果:

--- UnarySayHello client---
2023/02/26 22:03:35 failed to call SayHello: rpc error: code = Unauthenticated desc = 认证失败!
2023/02/26 22:03:35 could not greet: rpc error: code = Unauthenticated desc = 认证失败!
exit status 1

流式 RPC

(1)在任意目录下,分别创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 metadata.proto 文件,具体的目录结构如下所示:

StreamMetadata
├── client
│   └── proto
│       └── metadata.proto
└── server
    └── proto
        └── metadata.proto

metadata.proto 文件的具体内容如下所示:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package metadata; // 包名


// 定义服务
service Greeter {
    	// StreamSayHello 方法
    	rpc StreamSayHello (stream HelloRequest) returns (stream HelloResponse) {}
}

// 请求消息
message HelloRequest {
   	 	string name = 1;
}

// 响应消息
message HelloResponse {
    	string reply = 1;
}

(2)进入 proto 目录生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

StreamMetadata
├── client
│   └── proto
│       ├── metadata_grpc.pb.go
│       ├── metadata.pb.go
│       └── metadata.proto
└── server
    └── proto
        ├── metadata_grpc.pb.go
        ├── metadata.pb.go
        └── metadata.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序重写定义的方法,该程序的具体代码如下:

package main

import (
        "fmt"
        "time"
        "io"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/status"
        pb "server/proto"
        "net"
        "strconv"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc"
)


type server struct {
        pb.UnimplementedGreeterServer
}

// StreamSayHello 流式 RPC 调用客户端 metadata 操作
func (s *server) StreamSayHello(stream pb.Greeter_StreamSayHelloServer) error {
        // 在 defer 中创建 trailer 记录函数的返回时间
        defer func() {
                trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
                stream.SetTrailer(trailer)
        }()

        // 从 client 读取 metadata
        md, ok := metadata.FromIncomingContext(stream.Context())
        if !ok {
                return status.Errorf(codes.DataLoss, "StreamingSayHello: failed to get metadata")
        }

        if t, ok := md["token"]; ok {
                fmt.Printf("token from metadata:\n")
                if len(t) < 1 || t[0] != "cqupthao" {
                        return status.Error(codes.Unauthenticated, "认证失败!")
                }

                for i, e := range t {
                        fmt.Printf(" %d. %s\n", i, e)
                }
        }

        // 创建和发送 header
        header := metadata.New(map[string]string{"location": "CQ"})
        stream.SendHeader(header)

        // 读取请求数据发送响应数据.
        for {
                in, err := stream.Recv()
                if err == io.EOF {
                        return nil
                }
                if err != nil {
                        return err
                }
                fmt.Printf("request received %v, sending reply\n", in)
                if err := stream.Send(&pb.HelloResponse{Reply: in.Name}); err != nil {
                        return err
                }
        }
}

func main() {
        // 监听本地的 8972 端口
        lis, err := net.Listen("tcp", ":8972")
        if err != nil {
                fmt.Printf("failed to listen: %v", err)
                return
        }
        s := grpc.NewServer()                  // 创建 gRPC 服务器
        pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务
        // 启动服务
        err = s.Serve(lis)
        if err != nil {
                fmt.Printf("failed to serve: %v", err)
                return
        }
}

(4)在 client 目录下,编写 Client 端程序调用服务,该程序的具体代码如下:

package main

import (
        "context"
        "flag"
        "log"
        "fmt"
        "io"
        pb "client/proto"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)


const (
        defaultName = "cqupthao!"
)

var (
        addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
)

// StreamCallWithMetadata 流式 RPC 调用客户端 metadata 操作
func StreamCallWithMetadata(c pb.GreeterClient, name string) error {
        // 创建 metadata 和 context
        md := metadata.Pairs("token", "cqupthao")
        ctx := metadata.NewOutgoingContext(context.Background(), md)

        // 使用带有 metadata 的 context 执行 RPC 调用
        stream, err := c.StreamSayHello(ctx)
        if err != nil {
                log.Fatalf("failed to call StreaSayHello: %v\n", err)
        }

        go func() {
                // 当 header 到达时读取 header
                header, err := stream.Header()
                if err != nil {
                        log.Fatalf("failed to get header from stream: %v", err)
                }
                // 从返回响应的 header 中读取数据
                if l, ok := header["location"]; ok {
                        fmt.Printf("location from header:\n")
                        for i, e := range l {
                                fmt.Printf(" %d. %s\n", i, e)
                        }
                } else {
                        log.Println("location expected but doesn't exist in header")
                        return 
                }

                // 发送所有的请求数据到 server
                for i := 0; i < 3; i++ {
                        if err := stream.Send(&pb.HelloRequest{Name: name}); err != nil {
                                log.Fatalf("failed to send streaming: %v\n", err)
                        }
                }
                stream.CloseSend()
        }()

        // 读取所有的响应.
        var rpcStatus error
        fmt.Printf("get response:\n")
        for {
                r, err := stream.Recv()
                if err != nil {
                        rpcStatus = err
                        break
                }
                fmt.Printf(" - %s\n", r.Reply)
        }
        if rpcStatus != io.EOF {
                log.Printf("failed to finish server streaming: %v", rpcStatus)
        }

        // 当 RPC 结束时读取 trailer
        trailer := stream.Trailer()
        // 从返回响应的 trailer 中读取 metadata
        if t, ok := trailer["timestamp"]; ok {
                fmt.Printf("timestamp from trailer:\n")
                for i, e := range t {
                        fmt.Printf(" %d. %s\n", i, e)
                }
        } else {
                log.Printf("timestamp expected but doesn't exist in trailer")
        }

        return nil
}


func main() {
        flag.Parse()
        // 连接到 server 端,此处禁用安全传输
        conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        err = StreamCallWithMetadata(c , defaultName)
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
}

执行 Server 端和 Client 端的程序,服务端和客户端分别输出如下的结果:

// server
token from metadata:
 0. cqupthao
request received name:"cqupthao!", sending reply
request received name:"cqupthao!", sending reply
request received name:"cqupthao!", sending reply

// client
get response:
location from header:
 0. CQ
 - cqupthao!
 - cqupthao!
 - cqupthao!
timestamp from trailer:
 0. 1677420549

若传递错误的 Token ,则输出如下的结果:

get response:
2023/02/26 22:13:46 location expected but doesn't exist in header
2023/02/26 22:13:46 failed to finish server streaming: rpc error: code = Unauthenticated desc = 认证失败!
timestamp from trailer:
 0. 1677420826

  • 参考链接:gRPC 教程

  • 参考链接:gRPC 官网

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值