grpc 开发进阶 - 传递 metadata

Posted by icebergu on 07-07,2020

现在网上大部分都是 grpc 相关的介绍,真正涉及到 grpc 的配置使用的文章还是比较少的
所以本系列着重介绍 grpc 开发时可以能会用到的一些配置

grpc 支持在 server 端和 client 端发送 metedata,一些验证信息之类的可以放在这个里边

metadata

可以通过 metadata 包来构建

type MD map[string][]string

一个键可以对应多个值

生成 metadata
func New(m map[string]string) MD
func Pairs(kv ...string) MD


md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})

md = metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2",
)

key 中大写字母会被转化为小写
grpc- 开头的键为 grpc 内部使用,如果再 metadata 中设置这样的键可能会导致一些错误

存储二进制

metadata 中可以存储二进制数据
key 以 -bin 为后缀,这时,值会在传输前后以 base64 进行编解码

md := metadata.Pairs(
    "key", "string value",
    "key-bin", string([]byte{96, 102}), 
)
注意

服务端和客户端接收到的 metadata 如果被修改的话可能会导致 race
通过 Copy() 方法返回的 metadata 可以修改

func (md MD) Copy() MD

发送和接收 metadata

客户端

发送

有两种发送 metadata 到 server 的方法

推荐的方法是使用 AppendToOutgoingContext
如果 metadata 已存在则会合并,不存在则添加

func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context

// create a new context with some metadata
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// later, add some more metadata to the context (e.g. in an interceptor)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

NewOutgoingContext 则会覆盖 context 中 已有的 metadata

func NewOutgoingContext(ctx context.Context, md MD) context.Context

// create a new context with some metadata
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)

接收

client 和 server 接收 metadata 的方式是不同的,metadata 会被分为 headertriler

unary RPC 可以在调用的时候,使用 CallOption HeaderTrailer 来获取

```go
var header, trailer metadata.MD // variable to store header and trailer
r, err := client.SomeRPC(
    ctx,
    someRequest,
    grpc.Header(&header),    // will retrieve header
    grpc.Trailer(&trailer),  // will retrieve trailer
)

// do something with header and trailer

stream RPC 可以通过 ClientStream 接口的 Header, Trailer 方法来获取

stream, err := client.SomeStreamingRPC(ctx)

// retrieve header
header, err := stream.Header()

// retrieve trailer
trailer := stream.Trailer()

服务端

发送

server 端会把 metadata 分为 header 和 trailer 发送给 client

unary RPC 可以通过 CallOption grpc.SendHeadergrpc.SetTriler 来发送 header, trailer metadata

SetTriler 可以被调用多次,并且所有 metadata 会被合并
当 RPC 返回的时候, trailer metadata 会被发送

func SendHeader(ctx context.Context, md metadata.MD) error

func SetTrailer(ctx context.Context, md metadata.MD) error

func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    // create and send header
    header := metadata.Pairs("header-key", "val")
    grpc.SendHeader(ctx, header)
    // create and set trailer
    trailer := metadata.Pairs("trailer-key", "val")
    grpc.SetTrailer(ctx, trailer)
}

stream RPC 则可以直接使用 ServerStream 接口的方法

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    // create and send header
    header := metadata.Pairs("header-key", "val")
    stream.SendHeader(header)
    // create and set trailer
    trailer := metadata.Pairs("trailer-key", "val")
    stream.SetTrailer(trailer)
}
grpc.SendHeader 和 grpc.SetHeader 的区别

SendHeader 最多会被调用一次,提供的metadata会通过 SetHeader 来设置

SetHeader 可以被多次调用,所有的 metadata 都会被合并
当遇到以下行为的时候 metadata 会被发送

  • grpc.SendHeader 被调用
  • 第一个响应被发送
  • RPC status 被发送

接收

服务端调用 FromIncomingContext 即可从 context 中接收 client 发送的 metadata

func FromIncomingContext(ctx context.Context) (md MD, ok bool)


func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    md, ok := metadata.FromIncomingContext(stream.Context()) // get context from stream
    // do something with metadata
}

example: metadata