• gRPC 允许发送自定义元数据。元数据是键值对的一个非常简单的概念。
  • 在 Go gRPC 中,元数据是一个 map[string][]string。
  • 元数据可以作为 header 或 trailer 发送:
    • header 应该在数据之前发送。
    • trailer 应在处理完毕后发送。
  • 元数据允许我们在不更改 protobuf 消息的情况下向请求中添加数据。通常用于添加与请求相关但不属于请求的数据。
  • 例如,我们可以在请求的元数据中添加 JWT 令牌作为身份验证。这对于身份验证、速率限制或日志记录很有用。
  • 官方Go metadata文档:https://pkg.go.dev/google.golang.org/grpc/metadata

metadata 创建

  1. 使用 New():
md := metadata.New(map[string]string{"key1":"value1","key2":"value2"})
  1. 使用 Pairs():注意如果有相同的 key 会自动合并
md := metadata.Pairs(
    "key1", "value1",
    "key1", "value1.2", // "key1" will have map value []string{"value1", "value1.2"}
    "key2", "value2",
)
  1. 使用 Join():
md1 :=  metadata.Pairs("k1", "v1", "k2", "v2")
md2 := metadata.New(map[string]string{"key1":"value1","key2":"value2"})

md := metadata.Join(md1, md2)
  1. 存储二进制数据。
    • 在 metadata 中,key 永远是 string 类型,但是 value 可以是 string 也可以是二进制数据。为了在 metadata 中存储二进制数据,我们仅仅需要在 key 的后面加上一个 - bin 后缀。具有 - bin 后缀的 key 所对应的 value 在创建 metadata 时会被编码(base64),收到的时候会被解码:
md := metadata.Pairs(
    "key", "string value",
    "key-bin", string([]byte{96, 102}),
)

客户端发送 metadata

  1. client发送metadata,那就是把metadata存储到contex.Context。
  2. 把Metadata放到contex.Context,有几种方式。

NewOutgoingContext

  1. 将新创建的metadata添加到context中,这样会 覆盖 掉原来已有的metadata
// 将metadata添加到context中,获取新的context
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)

// unary RPC
response, err := client.SomeRPC(ctx, someRequest)

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

AppendToOutgoingContext

  1. 可以直接将 key-value 对添加到已有的context中
    • 如果context中没有metadata,那么就会 创建 一个
    • 如果已有metadata,那么就将数据 添加 到原来的metadata
// 如果对应的 context 没有 metadata,那么就会创建一个
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// 如果已有 metadata 了,那么就将数据添加到原来的 metadata  (例如在拦截器中)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

// 普通RPC(unary RPC)
response, err := client.SomeRPC(ctx, someRequest)

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

服务端接收 meatdata

  1. server接收metadata,就是从contex.Context中读取Metadata。
  2. 普通RPC与流式RPC的区别不大,都是从contex.Context中读取metadata。

FromIncomingContext

  1. 普通RPC(unary RPC)
//Unary Call
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}
  1. 流式RPC(streaming RPC)
//Streaming Call
func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    md, ok := metadata.FromIncomingContext(stream.Context()) // get context from stream
    // do something with metadata
}

服务端发送 metadata

  1. 服务端发送的metadata被分成了header和 trailer两者,因而客户端也可以读取两者。
  2. 对于普通RPC(unary RPC)server可以使用grpc包中提供的函数向client发送 header 和trailer。
    • grpc.SendHeader()
    • grpc.SetHeader()
    • grpc.SetTrailer()
  3. 对于流式RPC(streaming RPC)server可以使用ServerStream[1]接口中定义的函数向client发送header和 trailer。
    • ServerStream.SendHeader()
    • ServerStream.SetHeader()
    • ServerStream.SetTrailer()

普通RPC

  1. 使用 grpc.SendHeader() 和 grpc.SetTrailer() 方法 ,这两个函数将context.Context作为第一个参数
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
  // 创建并发送header
  header := metadata.Pairs("header-key", "val")
  grpc.SendHeader(ctx, header)
  
  // 创建并发送trailer
  trailer := metadata.Pairs("trailer-key", "val")
  grpc.SetTrailer(ctx, trailer)
}
  1. 如果不想立即发送header,也可以使用grpc.SetHeader()。grpc.SetHeader()可以被多次调用,在如下时机会把多个metadata合并发送出去。
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
  // 创建header,在适当时机会被发送
  header := metadata.Pairs("header-key1", "val1")
  grpc.SetHeader(ctx, header)
    
  // 创建header,在适当时机会被发送
  header := metadata.Pairs("header-key2", "val2")
  grpc.SetHeader(ctx, header)
  
  // 创建并发送trailer
  trailer := metadata.Pairs("trailer-key", "val")
  grpc.SetTrailer(ctx, trailer)
}

流式RPC

  1. 使用 ServerStream.SendHeader() 和 ServerStream.SetTrailer() 方法
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)
}
  1. 如果不想立即发送header,也可以使用ServerStream.SetHeader()。ServerStream.SetHeader()可以被多次调用,在如下时机会把多个metadata合并发送出去。
func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
  // create and send header
  header := metadata.Pairs("header-key", "val")
  stream.SetHeader(header)
  
  // create and set trailer
  trailer := metadata.Pairs("trailer-key", "val")
  stream.SetTrailer(trailer)
}

客户端接收 metadata

普通RPC

  1. 普通RPC(unary RPC)使用grpc.Header()和grpc.Trailer()方法来接收 Metadata
// RPC using the context with new metadata.
var header, trailer metadata.MD

// Add Order
order := pb.Order{Id: "101", Items: []string{"iPhone XS", "Mac Book Pro"}, Destination: "San Jose, CA", Price: 2300.00}
res, err := client.AddOrder(ctx, &order, grpc.Header(&header), grpc.Trailer(&trailer))
if err != nil {
  panic(err)
}

流式RPC

  1. 流式RPC(streaming RPC)通过调用返回的 ClientStream接口的Header()和 Trailer()方法接收 metadata
stream, err := client.SomeStreamingRPC(ctx)

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

stream.CloseAndRecv()

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

google.golang.org/grpc/metadata

func AppendToOutgoingContext

  1. AppendToOutgoingContext返回一个新的上下文,其中提供的kv与上下文中的任何现有元数据合并。
  2. 用于向现有的上下文中追加新的元数据。这通常用于在客户端发送请求时添加额外的元数据。
// AppendToOutgoingContext returns a new context with the provided kv merged
// with any existing metadata in the context. Please refer to the documentation
// of Pairs for a description of kv.
func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context {
    if len(kv)%2 == 1 {
        panic(fmt.Sprintf("metadata: AppendToOutgoingContext got an odd number of input pairs for metadata: %d", len(kv)))
    }
    md, _ := ctx.Value(mdOutgoingKey{}).(rawMD)
    added := make([][]string, len(md.added)+1)
    copy(added, md.added)
    kvCopy := make([]string, 0, len(kv))
    for i := 0; i < len(kv); i += 2 {
        kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
    }
    added[len(added)-1] = kvCopy
    return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md.md, added: added})
}
  1. 以下是 metadata.AppendToOutgoingContext 的使用方式:
package main

import (
    "context"
    "log"

    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
)

func main() {
    // 创建一个空元数据对象
    md := metadata.New(nil)

    // 添加自定义键值对
    md.Append("custom-header", "custom-value")

    // 创建一个上下文
    ctx := context.Background()

    // 将元数据追加到上下文中
    ctx = metadata.AppendToOutgoingContext(ctx, md)

    // 创建客户端连接
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithContextDialer(dialer))
    if err != nil {
        log.Fatalf("failed to dial: %v", err)
    }
    defer conn.Close()

    // 创建客户端实例
    c := NewYourServiceClient(conn)

    // 使用追加了元数据的上下文发送请求
    resp, err := c.YourRPCMethod(ctx, &YourRequest{})
    if err != nil {
        log.Fatalf("could not call: %v", err)
    }

    // 使用响应
    // ...
}
  1. 在这个例子中,我们首先创建了一个空的元数据对象 md,然后使用 metadata.Append 函数添加了一些自定义的键值对。接着,我们使用 metadata.AppendToOutgoingContext 函数将这个元数据追加到上下文中。最后,我们使用这个带有追加元数据的上下文来创建 gRPC 客户端并发送请求。
  2. 请注意,metadata.AppendToOutgoingContext 函数不会覆盖上下文中已存在的元数据,而是会追加新的元数据。如果你需要覆盖已存在的元数据,可以使用 metadata.NewOutgoingContext 函数。
// 如果对应的 context 没有 metadata,那么就会创建一个
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// 如果已有 metadata 了,那么就将数据添加到原来的 metadata  (例如在拦截器中)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

// 普通RPC(unary RPC)
response, err := client.SomeRPC(ctx, someRequest)

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

func NewOutgoingContext

  1. NewOutgoingContext创建一个带有外发md的新上下文。
  2. 如果与AppendToOutgoingContext一起使用,NewOutgoingContext将覆盖任何先前附加的元数据。
  3. 在调用此函数后不能修改Md。
  4. NewOutgoingContext 会覆盖之前数据,但是后面可以使用 AppendToOutgoingContext 追加数据。
// NewOutgoingContext creates a new context with outgoing md attached. If used
// in conjunction with AppendToOutgoingContext, NewOutgoingContext will
// overwrite any previously-appended metadata. md must not be modified after
// calling this function.
func NewOutgoingContext(ctx context.Context, md MD) context.Context {
    return context.WithValue(ctx, mdOutgoingKey{}, rawMD{md: md})
}
  1. 客户端使用示例:
// 将新创建的metadata添加到context中,这样会 覆盖 掉原来已有的metadata

// 将metadata添加到context中,获取新的context
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)

// unary RPC
response, err := client.SomeRPC(ctx, someRequest)

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

func NewIncomingContext

  1. NewIncomingContext创建一个带有传入md的新上下文。在调用此函数后不能修改Md。
  2. 该函数用于服务端,追加md到之前的ctx中。
// NewIncomingContext creates a new context with incoming md attached. md must
// not be modified after calling this function.
func NewIncomingContext(ctx context.Context, md MD) context.Context {
    return context.WithValue(ctx, mdIncomingKey{}, md)
}

func ValueFromIncomingContext

  1. ValueFromIncomingContext返回与传入元数据键对应的元数据值(如果存在)。键匹配不区分大小写。
  2. 该函数用于服务端,从传入的ctx中取出指定的key。
// ValueFromIncomingContext returns the metadata value corresponding to the metadata
// key from the incoming metadata if it exists. Keys are matched in a case insensitive
// manner.
func ValueFromIncomingContext(ctx context.Context, key string) []string {
	md, ok := ctx.Value(mdIncomingKey{}).(MD)
	if !ok {
		return nil
	}

	if v, ok := md[key]; ok {
		return copyOf(v)
	}
	for k, v := range md {
		// Case insensitive comparison: MD is a map, and there's no guarantee
		// that the MD attached to the context is created using our helper
		// functions.
		if strings.EqualFold(k, key) {
			return copyOf(v)
		}
	}
	return nil
}

type MD

  1. MD是从元数据键到值的映射。用户应该使用以下两个方便的函数New和Pairs来生成MD。
type MD map[string][]string

func FromIncomingContext

  1. 如果ctx存在,FromIncomingContext返回传入的元数据。
  2. 返回的MD中的所有键都是小写的。
  3. 该函数用于服务端从ctx中接收客户端传来的 Header。
// FromIncomingContext returns the incoming metadata in ctx if it exists.
//
// All keys in the returned MD are lowercase.
func FromIncomingContext(ctx context.Context) (MD, bool) {
    md, ok := ctx.Value(mdIncomingKey{}).(MD)
    if !ok {
        return nil, false
    }
    out := make(MD, len(md))
    for k, v := range md {
        // We need to manually convert all keys to lower case, because MD is a
        // map, and there's no guarantee that the MD attached to the context is
        // created using our helper functions.
        key := strings.ToLower(k)
        out[key] = copyOf(v)
    }
    return out, true
}
  1. 服务端使用:
// 从传入上下文获取元数据
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
    return nil, fmt.Errorf("上下文中没有元数据")
}

func FromOutgoingContext

  1. FromOutgoingContext返回ctx形式的传出元数据(如果存在)。
  2. 返回的MD中的所有键都是小写的。
  3. 用于客户端,比如客户端拦截器中,获取发送给服务器的 metadata。
// FromOutgoingContext returns the outgoing metadata in ctx if it exists.
//
// All keys in the returned MD are lowercase.
func FromOutgoingContext(ctx context.Context) (MD, bool) {
    raw, ok := ctx.Value(mdOutgoingKey{}).(rawMD)
    if !ok {
        return nil, false
    }

    mdSize := len(raw.md)
    for i := range raw.added {
        mdSize += len(raw.added[i]) / 2
    }

    out := make(MD, mdSize)
    for k, v := range raw.md {
        // We need to manually convert all keys to lower case, because MD is a
        // map, and there's no guarantee that the MD attached to the context is
        // created using our helper functions.
        key := strings.ToLower(k)
        out[key] = copyOf(v)
    }
    for _, added := range raw.added {
        if len(added)%2 == 1 {
            panic(fmt.Sprintf("metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d", len(added)))
        }

        for i := 0; i < len(added); i += 2 {
            key := strings.ToLower(added[i])
            out[key] = append(out[key], added[i+1])
        }
    }
    return out, ok
}
  1. 客户端拦截器中:
func orderUnaryClientInterceptor(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    var s string

    // 获取要发送给服务端的`metadata`
    md, ok := metadata.FromOutgoingContext(ctx)
    if ok && len(md.Get("time")) > 0 {
        s = md.Get("time")[0]
    } else {
        // 如果没有则补充这个时间戳字段
        s = "inter" + strconv.FormatInt(time.Now().UnixNano(), 10)
        ctx = metadata.AppendToOutgoingContext(ctx, "time", s)
    }

    log.Printf("call timestamp: %s", s)

    // Invoking the remote method
    err := invoker(ctx, method, req, reply, cc, opts...)

    return err
}

func New

  1. New从给定的键值映射创建一个MD。
  2. key中只允许使用以下ASCII字符:
    • 数字:0-9
    • 大写字母:A-Z
    • 小写字母:a-z
    • 特殊字符:- _ .
  3. 大写字母会自动转换为小写字母。
  4. 以“grpc-”开头的键仅供grpc内部使用,如果在元数据中设置可能会导致错误。
// New creates an MD from a given key-value map.
//
// Only the following ASCII characters are allowed in keys:
//   - digits: 0-9
//   - uppercase letters: A-Z (normalized to lower)
//   - lowercase letters: a-z
//   - special characters: -_.
//
// Uppercase letters are automatically converted to lowercase.
//
// Keys beginning with "grpc-" are reserved for grpc-internal use only and may
// result in errors if set in metadata.
func New(m map[string]string) MD {
    md := make(MD, len(m)) // map[string][]string
    for k, val := range m { // string,string
        key := strings.ToLower(k)
        md[key] = append(md[key], val)
    }
    return md
}
  1. 使用示例:
md := metadata.New(map[string]string{"key1":"value1","key2":"value2"})

func Pairs

  1. pair返回一个由key, value…如果len(kv)是奇数,Pairs 就会 panic。
  2. key中只允许使用以下ASCII字符:
    • 数字:0-9
      • 大写字母:A-Z
      • 小写字母:a-z
      • 特殊字符:- _ .
  3. 大写字母会自动转换为小写字母。
  4. 以“grpc-”开头的键仅供grpc内部使用,如果在元数据中设置可能会导致错误。
  5. 如果有相同的 key 会自动合并。
// Pairs returns an MD formed by the mapping of key, value ...
// Pairs panics if len(kv) is odd.
//
// Only the following ASCII characters are allowed in keys:
//   - digits: 0-9
//   - uppercase letters: A-Z (normalized to lower)
//   - lowercase letters: a-z
//   - special characters: -_.
//
// Uppercase letters are automatically converted to lowercase.
//
// Keys beginning with "grpc-" are reserved for grpc-internal use only and may
// result in errors if set in metadata.
func Pairs(kv ...string) MD {
    if len(kv)%2 == 1 {
        panic(fmt.Sprintf("metadata: Pairs got the odd number of input pairs for metadata: %d", len(kv)))
    }
    md := make(MD, len(kv)/2) // map[string][]string
    for i := 0; i < len(kv); i += 2 {
        key := strings.ToLower(kv[i])
        md[key] = append(md[key], kv[i+1])
    }
    return md
}
  1. 使用示例:
md := metadata.Pairs(
    "key1", "value1",
    "key1", "value1.2", // "key1" will have map value []string{"value1", "value1.2"}
    "key2", "value2",
)

func Join

  1. Join将任意数量的mds连接到单个MD中。
  2. 每个键的值顺序由包含这些值的mds呈现给Join的顺序决定。
// Join joins any number of mds into a single MD.
//
// The order of values for each key is determined by the order in which the mds
// containing those values are presented to Join.
func Join(mds ...MD) MD {
    out := MD{} // []map[string][]string
    for _, md := range mds { // int,map[string][]string
        for k, v := range md { // string,[]string
            out[k] = append(out[k], v...)
        }
    }
    return out
}
  1. 使用示例:
md1 :=  metadata.Pairs("k1", "v1", "k2", "v2")
md2 := metadata.New(map[string]string{"key1":"value1","key2":"value2"})

md := metadata.Join(md1, md2)

func (MD) Append

  1. Append将值添加到键k中,而不是覆盖已经存储在该键中的内容。
  2. K在存储到md之前被转换为小写。
// Append adds the values to key k, not overwriting what was already stored at
// that key.
//
// k is converted to lowercase before storing in md.
func (md MD) Append(k string, vals ...string) {
    if len(vals) == 0 {
        return
    }
    k = strings.ToLower(k)
    md[k] = append(md[k], vals...)
}
  1. 使用示例:
md := metadata.New(map[string]string{"key1":"value1","key2":"value2"})

md.Append("key1", "v1", "v2")

func (MD) Copy

  1. Copy返回md的副本。
// Copy returns a copy of md.
func (md MD) Copy() MD {
    out := make(MD, len(md))
    for k, v := range md {
        out[k] = copyOf(v)
    }
    return out
}

func (MD) Delete

  1. Delete删除给定键k的值,该键k在从md中删除之前被转换为小写。
// Delete removes the values for a given key k which is converted to lowercase
// before removing it from md.
func (md MD) Delete(k string) {
    k = strings.ToLower(k)
    delete(md, k)
}

func (MD) Get

  1. Get获取给定键的值。
  2. 在md中搜索之前,K被转换为小写。
// Get obtains the values for a given key.
//
// k is converted to lowercase before searching in md.
func (md MD) Get(k string) []string {
    k = strings.ToLower(k)
    return md[k]
}

func (MD) Len

  1. Len返回md中的项数。
// Len returns the number of items in md.
func (md MD) Len() int {
    return len(md)
}

func (MD) Set

  1. Set用一个值片来设置给定键的值。
  2. K在存储到md之前被转换为小写。
// Set sets the value of a given key with a slice of values.
//
// k is converted to lowercase before storing in md.
func (md MD) Set(k string, vals ...string) {
    if len(vals) == 0 {
        return
    }
    k = strings.ToLower(k)
    md[k] = vals
}

参考

  1. 写给go开发者的gRPC教程-metadata