Golang 中 gRPC 的 metadata

667 阅读2分钟

1. metadata 介绍

在 HTTP/1.1 中,我们常常通过直接操纵 Header 来传递数据,而对于 gRPC 来讲,因为它是基于 HTTP/2 的,所以本质上也可以通过 Header 来传递数据,但不是直接去操纵它,而是通过 gRPC 中的 metadata 来调用过程中的数据进行传递和操纵的。

metadata 是以 key-value 的形式存储数据的,其中 key 是 string 类型,而 value 是 []string 类型,即一个字符串数组。metadata 使得 client 和 server 能够为对方提供关于本次调用的一些信息,就像一次 http 请求的 Request Header 和 Response Header 一样。http 中Header 的声明周期是一次 http 请求,那么 metadata 的生命周期就是一次 RPC 调用。

在 gRPC 中,metadata 实际上就是一个 map 结构,原型如下:

type MD map[string][]string

2. 创建 metadata

google.golang.org/grpc/metadata 源码中提供了两个方法来创建 metadata。

  • metadata.New
  • metadata.Pairs

2.1 metadata.New

metadata.New(map[string]string{"name":"bobo", "password":"123456"})

使用 New 方法创建的 metadata 会直接被转换为对应的 MD 结构,并且所有的 key 默认都被小写,参考结果如下:

name: []string{"bobo"}
password: []string{"123456"}

我们也可以看一下 metadata.New 的源码。

func New(m map[string]string) MD {
	md := MD{}
	for k, val := range m {
		key := strings.ToLower(k)
		md[key] = append(md[key], val)
	}
	return md
}

2.2 metadata.Pairs

metadata.Pairs(
    "name", "bobo",
    "password", "123456",
    "name", "张三",
)

使用 Pairs 方法创建的 metadata 会以奇数来配对,并且所有的 key 默认都被小写,若出现同名的 key,则会追加到对应 key 的切片上,参考结果如下:

name: []string{"bobo", "张三"}
password: []string{"123456"}

我们也可以看一下 metadata.Pairs 的源码。

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 := MD{}
	for i := 0; i < len(kv); i += 2 {
		key := strings.ToLower(kv[i])
		md[key] = append(md[key], kv[i+1])
	}
	return md
}

3. 设置/获取 metadata

3.1 设置 metadata

md := metadata.Pairs(
    "name", "bobo",
    "password", "123456",
)

//新建一个有 metadata 的context
ctx := metadata.NewOutgoingContext(context.Background(), md)

//单项 RPC
response, err := client.SayHello(ctx, &proto.HelloRequest{
		Name: "AI",
	})

3.2 获取 metadata

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
	//获取 metadata
    md, ok := metadata.FromIncomingContext(ctx)
    
	return &proto.HelloReply{
		Message: "hello " + request.Name,
	}, nil
}

4. metadata 使用

4.1 Proto

syntax = "proto3";

//设置 pb.go 文件的保存目录和包名
option go_package = "proto;proto";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

4.2 Server 服务端

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"net"
	"protobuf-demo/proto"
)

type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		fmt.Println("get metadata error")
	}
	if names := md.Get("name"); len(names) > 0 {
		fmt.Printf("name=%s\n", names[0])
	}
	if ps := md.Get("password"); len(ps) > 0 {
		fmt.Printf("password=%s\n", ps[0])
	}
	return &proto.HelloReply{
		Message: "hello " + request.Name,
	}, nil
}

func main() {
	g := grpc.NewServer()
	proto.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic("failed to listen:" + err.Error())
	}
	err = g.Serve(lis)
	if err != nil {
		panic("failed to start grpc:" + err.Error())
	}
}

4.3 Client 客户端

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"protobuf-demo/proto"
)

func main() {
	var opts = []grpc.DialOption{
		grpc.WithInsecure(),
	}
	conn, err := grpc.Dial("127.0.0.1:8080", opts...)
	defer conn.Close()
	if err != nil {
		panic(err)
	}

	md := metadata.Pairs("name", "bobo", "password", "123456")
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	client := proto.NewGreeterClient(conn)
	r, err := client.SayHello(ctx, &proto.HelloRequest{
		Name: "AI",
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(r.Message)
}