grpc元数据

gRPC Metadata(元数据) 是 gRPC 通信中非常关键的概念。如果说 Protobuf 消息是信件内容,那么 Metadata 就是信封上的信息(如:寄件人、收件人、加急标记、邮戳)。

它主要用于传递 非业务数据,例如:认证 Token、链路追踪 ID、语言设置、请求来源等。


第一部分:核心概念(通俗版)

  1. **键值对 (Key-Value)**:Metadata 本质是 map[string][]string
    • Key:必须是 ASCII 小写字符(如 authorization, user-id)。
    • Value:是字符串切片 []string。这意味着一个 Key 可以对应多个 Value。
  2. 绑定 Context:Metadata 不能单独发送,必须附着在 context.Context 上。
  3. 方向性
    • **Outgoing (发出)**:客户端发给服务端,或服务端回给客户端。
    • **Incoming (接收)**:服务端收到客户端的,或客户端收到服务端的。
  4. 特殊后缀:如果 Key 以 -bin 结尾(如 token-bin),表示这是二进制数据,gRPC 会自动进行 Base64 编码。

第二部分:环境准备

沿用之前的项目结构,定义一个简单的 Proto。

文件路径: proto/meta.proto

1
2
3
4
5
6
7
8
9
syntax = "proto3";
package meta;

message Empty {}
message Response { string result = 1; }

service MetaService {
rpc GetInfo (Empty) returns (Response);
}

(记得运行 protoc 生成 Go 代码)


第三部分:完整 Demo 演示

Demo 1:客户端发送,服务端接收 (最常用)

场景:客户端在请求头中携带 authorization (Token) 和 request-id (追踪 ID),服务端解析并打印。

1. 客户端 (client_send.go)
重点:metadata.Pairs 创建数据,metadata.NewOutgoingContext 绑定上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"context"
"log"
"example.com/grpc-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)

func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()

client := proto.NewMetaServiceClient(conn)

// 1. 创建元数据 (Key-Value)
// 注意:Value 是 []string,所以 "token" 对应 []string{"bearer-123"}
md := metadata.Pairs(
"authorization", "bearer-123",
"request-id", "uuid-999",
"user-id", "1001", "1002", // 演示一个 Key 对应多个 Value
)

// 2. 将元数据注入到 Context 中
// 关键:NewOutgoingContext 返回一个新的 context,必须使用这个新的!
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 3. 发起请求 (使用带有 metadata 的 ctx)
_, err := client.GetInfo(ctx, &proto.Empty{})
if err != nil {
log.Fatalf("请求失败:%v", err)
}
log.Println("请求发送成功")
}

2. 服务端 (server_receive.go)
*重点:`metadata.FromIncomingContext
你好!gRPC Metadata(元数据) 是 gRPC 通信中非常关键的概念。如果说 Protobuf 消息是信件内容,那么 Metadata 就是信封上的信息(如:寄件人、收件人、加急标记、邮戳)。

它主要用于传递 非业务数据,例如:认证 Token、链路追踪 ID、语言设置、请求来源等。

本节课我将通过 3 个循序渐进的 Demo,带你彻底搞懂 Metadata 的发送、接收、以及响应头处理。


第一部分:核心概念(通俗版)

  1. **键值对 (Key-Value)**:Metadata 本质是 map[string][]string
    • Key:必须是 ASCII 小写字符(如 authorization, user-id)。
    • Value:是字符串切片 []string。这意味着一个 Key 可以对应多个 Value。
  2. 绑定 Context:Metadata 不能单独发送,必须附着在 context.Context 上。
  3. 方向性
    • **Outgoing (发出)**:客户端发给服务端,或服务端回给客户端。
    • **Incoming (接收)**:服务端收到客户端的,或客户端收到服务端的。
  4. 特殊后缀:如果 Key 以 -bin 结尾(如 token-bin),表示这是二进制数据,gRPC 会自动进行 Base64 编码。

第二部分:环境准备

沿用之前的项目结构,定义一个简单的 Proto。

文件路径: proto/meta.proto

1
2
3
4
5
6
7
8
9
syntax = "proto3";
package meta;

message Empty {}
message Response { string result = 1; }

service MetaService {
rpc GetInfo (Empty) returns (Response);
}

(记得运行 protoc 生成 Go 代码)


第三部分:完整 Demo 演示

Demo 1:客户端发送,服务端接收 (最常用)

场景:客户端在请求头中携带 authorization (Token) 和 request-id (追踪 ID),服务端解析并打印。

1. 客户端 (client_send.go)
重点:metadata.Pairs 创建数据,metadata.NewOutgoingContext 绑定上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"context"
"log"
"example.com/grpc-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)

func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()

client := proto.NewMetaServiceClient(conn)

// 1. 创建元数据 (Key-Value)
// 注意:Value 是 []string,所以 "token" 对应 []string{"bearer-123"}
md := metadata.Pairs(
"authorization", "bearer-123",
"request-id", "uuid-999",
"user-id", "1001", "1002", // 演示一个 Key 对应多个 Value
)

// 2. 将元数据注入到 Context 中
// 关键:NewOutgoingContext 返回一个新的 context,必须使用这个新的!
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 3. 发起请求 (使用带有 metadata 的 ctx)
_, err := client.GetInfo(ctx, &proto.Empty{})
if err != nil {
log.Fatalf("请求失败:%v", err)
}
log.Println("请求发送成功")
}

2. 服务端 (server_receive.go)
重点:metadata.FromIncomingContext 提取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"context"
"log"
"net"
"example.com/grpc-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)

type Server struct {
proto.UnimplementedMetaServiceServer
}

func (s *Server) GetInfo(ctx context.Context, req *proto.Empty) (*proto.Response, error) {
// 1. 从 Context 中提取元数据
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, grpc.Errorf(grpc.Code(grpc.Internal), "无法获取元数据")
}

// 2. 读取具体的值
// md.Get(key) 返回 []string
tokens := md.Get("authorization")
reqIds := md.Get("request-id")
userIds := md.Get("user-id")

log.Printf("收到 Token: %v", tokens) // [bearer-123]
log.Printf("收到 RequestID: %v", reqIds) // [uuid-999]
log.Printf("收到 UserIDs: %v", userIds) // [1001 1002]

return &proto.Response{Result: "OK"}, nil
}

func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
proto.RegisterMetaServiceServer(s, &Server{})
log.Println("服务端启动 :50051")
s.Serve(lis)
}

运行结果:
服务端日志会清晰打印出客户端传来的所有元数据。


Demo 2:服务端发送响应元数据,客户端接收

场景:服务端处理完请求后,想在 响应头 (Header)响应尾 (Trailer) 中返回一些信息(如:剩余配额、处理耗时),而不是放在业务消息体里。

1. 服务端 (server_send_resp.go)
重点:使用 grpc.SendHeadergrpc.SetTrailer
注意:在 Unary 模式下,直接操作 Stream 比较麻烦,通常建议在 拦截器 中设置响应头。但为了演示 API,这里展示如何在 Handler 中通过 grpc.SetHeader 设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (s *Server) GetInfo(ctx context.Context, req *proto.Empty) (*proto.Response, error) {
// 1. 设置响应头 (Header) - 在发送响应前
// 必须使用 grpc.SetHeader,它会将数据写入 ctx 关联的 stream 中
err := grpc.SetHeader(ctx, metadata.Pairs("x-rate-limit-remaining", "100"))
if err != nil {
return nil, err
}

// 2. 设置响应尾 (Trailer) - 在请求处理完成后
// 通常用于返回处理结果状态,如处理耗时
grpc.SetTrailer(ctx, metadata.Pairs("x-process-time-ms", "50"))

return &proto.Response{Result: "Success"}, nil
}

2. 客户端 (client_recv_resp.go)
重点:使用 grpc.Headergrpc.Trailer CallOption 来捕获响应元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := proto.NewMetaServiceClient(conn)

// 1. 准备变量来接收元数据
var headerMd, trailerMd metadata.MD

// 2. 发起请求,并传入 CallOption 来捕获元数据
_, err := client.GetInfo(
context.Background(),
&proto.Empty{},
grpc.Header(&headerMd), // 捕获响应头
grpc.Trailer(&trailerMd), // 捕获响应尾
)
if err != nil {
log.Fatalf("请求失败:%v", err)
}

// 3. 打印收到的响应元数据
log.Printf("响应头:%v", headerMd) // map[x-rate-limit-remaining:[100]]
log.Printf("响应尾:%v", trailerMd) // map[x-process-time-ms:[50]]
}

Demo 3:二进制元数据 (Binary Metadata)

场景:传输非字符串数据(如 Protobuf 序列化的对象、加密的 Token)。
规则:Key 必须以 -bin 结尾。gRPC 会自动帮你做 Base64 编码/解码。

1. 客户端

1
2
3
4
5
6
7
// 原始二进制数据
rawData := []byte{0x01, 0x02, 0x03}

// Key 必须以 -bin 结尾
md := metadata.Pairs("custom-data-bin", string(rawData))
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 发送...

2. 服务端

1
2
3
4
5
6
7
md, _ := metadata.FromIncomingContext(ctx)
vals := md.Get("custom-data-bin")
// gRPC 已经自动帮你 Base64 解码了,直接转回 []byte 即可
// 注意:md.Get 返回的是 string,如果是 -bin,底层库处理过编码,
// 但取出来时通常还是 string 形式,需要根据实际情况处理。
// 修正:grpc-go 库中,-bin 结尾的 key 取出来是解码后的二进制字符串。
data := []byte(vals[0])

第四部分:常见陷阱与最佳实践 (Pitfalls)

这部分非常重要,能帮你避开 90% 的坑。

1. Context 不可变 (Immutable)

  • 错误写法

    1
    2
    3
    ctx := context.Background()
    metadata.NewOutgoingContext(ctx, md) // 返回值被忽略了!
    client.Do(ctx) // 这里的 ctx 没有元数据!
  • 正确写法

    1
    2
    3
    ctx := context.Background()
    ctx = metadata.NewOutgoingContext(ctx, md) // 必须接收返回值
    client.Do(ctx)

2. 追加 vs 覆盖 (Append vs Replace)

  • metadata.NewOutgoingContext覆盖 当前 Context 中已有的同 Key 元数据。

  • 如果你想 追加 (保留旧的,增加新的),请使用 metadata.AppendToOutgoingContext

    1
    2
    3
    4
    5
    // 覆盖
    ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("token", "new"))

    // 追加 (token 列表里会有 old 和 new)
    ctx = metadata.AppendToOutgoingContext(ctx, "token", "new")

3. Key 的大小写

  • HTTP/2 要求 Header Key 必须是 小写
  • gRPC 会自动将 Key 转为小写。
  • 建议:自己写代码时永远用小写,如 user-id 而不是 User-Id

4. 不要在 Metadata 中放业务数据

  • 错误:把 OrderID, ProductPrice 放在 Metadata 里。
  • 正确:这些应该放在 Protobuf 消息体里。Metadata 仅用于 横切关注点 (认证、追踪、限流)。

5. 拦截器中的 Metadata

  • 在拦截器中修改 Metadata 是最常见的用法。
  • 如果在拦截器中添加了 Metadata,记得把新的 Context 传给 handler(ctx, req)

第五部分:总结归纳表

操作方向 场景 核心 API 注意事项
创建 构建元数据 metadata.Pairs(k, v, ...) Value 是 []string
创建 构建二进制元数据 metadata.Pairs("key-bin", val) Key 必须以 -bin 结尾
客户端发送 将元数据放入请求 metadata.NewOutgoingContext(ctx, md) 必须接收返回的 ctx
客户端发送 追加元数据 metadata.AppendToOutgoingContext(ctx, k, v) 不覆盖已有的
服务端接收 从请求中提取 metadata.FromIncomingContext(ctx) 返回 (md, ok)
服务端发送 设置响应头 (Header) grpc.SetHeader(ctx, md) 需在返回响应前调用
服务端发送 设置响应尾 (Trailer) grpc.SetTrailer(ctx, md) 请求结束后发送
客户端接收 获取响应头/尾 grpc.Header(&md), grpc.Trailer(&md) 作为 CallOption 传入
通用 合并元数据 metadata.Join(md1, md2) 合并两个 MD 对象

学习路线建议

  1. 先掌握 Demo 1:这是 90% 的场景(客户端传 Token,服务端校验)。
  2. 理解 Context:明白 Metadata 是存在 Context 里的,Context 是传递的载体。
  3. 结合拦截器:实际开发中,你很少在业务代码里手动写 metadata.NewOutgoingContext,通常是在 客户端拦截器 里统一注入 Token,在 服务端拦截器 里统一校验。
  4. 了解响应头:当需要获取服务端返回的限流信息、TraceID 时,再学习 Demo 2 的 grpc.Header 用法。
[up主专用,视频内嵌代码贴在这]