在分布式系统中,客户端和服务端之间的通信不仅仅是数据的交换,还涉及到身份验证、日志追踪等额外信息的传递。gRPC 提供了一种名为 Metadata
的机制来满足这种需求。本文将通过一个具体的示例来讲解如何在 Go 语言中使用 gRPC 的 Metadata。
一、简介
Metadata 是一种键值对结构,它可以在不改变请求或响应消息体的情况下携带额外的信息。这些信息通常用于认证(如 token)、追踪(如 trace id)等场景。值得注意的是,Metadata 键是大小写不敏感的,并且仅支持 ASCII 字符串作为键名。
二、示例代码概览
本文使用的示例是一个简单的问候服务,它不仅返回问候消息,还会打印从客户端传来的元数据。这个例子包括了服务端和客户端两部分代码。
三、Protocol Buffer 定义(.proto
文件)
syntax = "proto3";
option go_package = ".;proto"; // 生成的 Go 代码所在的包名是 proto
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply); // 一个远程方法
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
解释:
- 定义了一个服务接口
Greeter
,其中有一个远程调用方法SayHello
。 - 请求参数是
HelloRequest
,包含字段name
。 - 返回值是
HelloReply
,包含字段message
。
这个 .proto
文件会被编译成 Go 代码供服务端和客户端使用。
四、服务端代码详解
package main
import (
"GolandProjects/awesomeProject1/grpc_test2/metadata_test/proto"
"context"
"fmt"
"net"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc"
)
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 nameSlice, ok := md["name"]; ok {
fmt.Println(nameSlice)
for i, e := range nameSlice {
fmt.Println(i, e)
}
}
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:50051")
if err != nil {
panic("failed to listen:" + err.Error())
}
err = g.Serve(lis)
if err != nil {
panic("failed to start grpc:" + err.Error())
}
}
功能说明:
1. 创建并启动 gRPC 服务
g := grpc.NewServer()
proto.RegisterGreeterServer(g, &Server{})
lis, err := net.Listen("tcp", "0.0.0.0:50051")
err = g.Serve(lis)
- 启动一个 gRPC 服务器监听
50051
端口。 - 注册
Greeter
服务到服务器上。
2. SayHello
方法实现
func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply, error)
- 这是客户端调用的方法。
- 接收请求参数
request
,返回响应HelloReply
。
获取元数据(Metadata)
md, ok := metadata.FromIncomingContext(ctx)
if ok {
fmt.Println("get metadata error")
}
- 从上下文
ctx
中获取客户端传来的 Metadata。 - 如果没有 Metadata,
ok == false
,否则可以读取。
if nameSlice, ok := md["name"]; ok {
fmt.Println(nameSlice)
for i, e := range nameSlice {
fmt.Println(i, e)
}
}
- 检查是否有键为
"name"
的 Metadata。 - 输出它的内容(注意 Metadata 是字符串切片)。
构造返回值
return &proto.HelloReply{
Message: "hello " + request.Name,
}, nil
- 返回一条问候语。
五、客户端代码详解
package main
import (
"GolandProjects/awesomeProject1/grpc_test2/metadata_test/proto"
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
md := metadata.New(map[string]string{
"name": "bobby",
"pasword": "imooc",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
r, err := c.SayHello(ctx, &proto.HelloRequest{Name: "bobby"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
功能说明:
1. 建立 gRPC 连接
conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())
- 连接到本地运行的 gRPC 服务。
grpc.WithInsecure()
表示不启用 TLS 加密。
2. 创建客户端对象
c := proto.NewGreeterClient(conn)
- 使用连接创建一个客户端实例。
3. 添加 Metadata 到请求上下文中
md := metadata.New(map[string]string{
"name": "bobby",
"pasword": "imooc",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
- 创建 Metadata,包含两个键值对。
- 将其绑定到新的上下文
ctx
上,用于后续 RPC 调用。
⚠️ 注意:gRPC 中 Metadata 是 HTTP Headers 的一种抽象,在 gRPC 中作为附加信息传递。
4. 发起远程调用
r, err := c.SayHello(ctx, &proto.HelloRequest{Name: "bobby"})
- 调用服务端的
SayHello
方法。 - 传入请求参数和带有 Metadata 的上下文。
5. 输出响应结果
fmt.Println(r.Message)
- 打印服务端返回的问候语。
服务器端实现的主要步骤:
- 创建一个结构体
Server
,实现Greeter
服务接口中定义的SayHello
方法。 - 在
SayHello
方法中,从上下文ctx
中提取元数据,并打印出来。 - 返回一个包含问候消息的
HelloReply
消息。 - 在
main
函数中,创建 gRPC 服务器,注册服务实现,创建 TCP 监听器,并启动服务器
客户端实现的主要步骤:
- 连接到 gRPC 服务器(使用
grpc.Dial
)。 - 创建
Greeter
服务的客户端(使用proto.NewGreeterClient
)。 - 创建元数据(使用
metadata.New
)。 - 将元数据添加到上下文(使用
metadata.NewOutgoingContext
)。 - 调用服务方法(使用
c.SayHello
)。 - 处理响应并打印结果。
六、总结
关于 Metadata 的说明
- 在 gRPC 中,Metadata 是 key-value 形式的附加信息。
- 可以用来传输 session id、认证 token、trace id 等信息。
- 不属于请求体,而是类似于 HTTP Header 的存在。
- 支持多个值(例如
Set-Cookie
),因此是[]string
类型。
常见用途举例
- 用户身份验证(token)
- 日志追踪 ID(trace-id, span-id)
- 客户端版本信息
- Session ID(如题中提到的“将 sessionid 放入 cookie”)
❗ 注意:gRPC 中没有直接的 Cookie 支持(HTTP 1.1 特性),但可以通过 Metadata 模拟。
最佳实践建议
- Metadata 键名推荐小写(避免大小写问题)。
- 敏感信息如密码应加密或避免通过 Metadata 传输。
- 使用
metadata.AppendToMD
或metadata.Pairs
来构造更复杂的 Metadata。
Metadata 提供了一种灵活的方式,让开发者能够在不影响原有协议的前提下,增加额外的功能特性,比如安全性和可追踪性。