本篇为【写给go开发者的gRPC教程系列】第五篇
第一篇:protobuf基础
第二篇:通信模式
第三篇:拦截器
第四篇:错误处理
第五篇:metadata 👈
本系列将持续更新,欢迎关注👏获取实时通知
导语
和在普通
HTTP
请求中一样,gRPC提供了在每一次RPC中携带的上下文结构:metadata
。在Go语言中,它与context.Context
紧密结合,帮助我们实现服务端与客户端之间互相传递信息
什么是metadata
?
gRPC 的 metadata
简单理解,就是 HTTP Header
中的 key-value 对
metadata
是以key-value
的形式存储数据的,其中 key 是 string 类型,而 value 是 []string,即一个字符串数组类型metadata
使得 client 和 server 能够为对方提供关于本次调用的一些信息,就像一次HTTP请求的Request Header
和Response Header
一样HTTP Header
的生命周期是一次 HTTP 请求,那么metadata
的生命周期就是一次 RPC 调用
Metadata 创建
🌲 使用New():
md := metadata.New(map[string]string{"key1":"value1","key2":"value2"})
🌲 使用Pairs():
要注意如果有相同的 key 会自动合并
md := metadata.Pairs(
"key1", "value1",
"key1", "value1.2", // "key1" will have map value []string{"value1", "value1.2"}
"key2", "value2",
)
🌲 合并多个metadata
md1 := metadata.Pairs("k1", "v1", "k2", "v2")
md2 := metadata.New(map[string]string{"key1":"value1","key2":"value2"})
md := metadata.Join(md1, md2)
🌲 存储二进制数据
在 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 结构本身也有一些操作方法,参考文档非常容易理解。这里不再赘述:https://pkg.go.dev/google.gol...
Metadata 发送与接收
让我们再次回顾下pb文件和生成出来的client与server端的接口
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order);
}
type OrderManagementClient interface {
GetOrder(ctx context.Context,
in *wrapperspb.StringValue, opts ...grpc.CallOption) (*Order, error)
}
type OrderManagementServer interface {
GetOrder(context.Context, *wrapperspb.StringValue) (*Order, error)
mustEmbedUnimplementedOrderManagementServer()
}
可以看到相比pb中的接口定义,生成出来的Go代码除了增加了error
返回值,还多了context.Context
和错误处理类似,gRPC中的context.Context
也符合Go语言的使用习惯:通常情况下我们在函数首个参数放置context.Context
用来传递一次RPC中有关的上下文,借助context.WithValue()
或ctx.Value()
往context
添加变量或读取变量
metadata
就是gRPC中可以传递的上下文信息之一,所以metadata
的使用方式就是:metadata
记录到context
,从context
读取metadata
Clinet发送Server接收
client
发送metadata
,那就是把metadata
存储到contex.Context
server
接收metadata
,就是从contex.Context
中读取Metadata
Clinet 发送 Metadata
把Metadata
放到contex.Context
,有几种方式
🌲 使用NewOutgoingContext
将新创建的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
可以直接将 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)
Server 接收 Metedata
普通RPC与流式RPC的区别不大,都是从contex.Context
中读取metadata
🌲 使用FromIncomingContext
普通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
}
流式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
}
Server发送Clinet接收
服务端发送的metadata
被分成了header
和 trailer
两者,因而客户端也可以读取两者
Server 发送 Metadata
对于普通RPC(unary RPC)server可以使用grpc包中提供的函数向client发送 header
和trailer
grpc.SendHeader()
grpc.SetHeader()
grpc.SetTrailer()
对于**流式RPC(streaming RPC)server可以使用ServerStream接口中定义的函数向client发送header
和 trailer
ServerStream.SendHeader()
ServerStream.SetHeader()
ServerStream.SetTrailer()
🌲 普通RPC(unary RPC)
使用 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)
}
如果不想立即发送header
,也可以使用grpc.SetHeader()
。grpc.SetHeader()
可以被多次调用,在如下时机会把多个metadata
合并发送出去
- 调用
grpc.SendHeader()
- 第一个响应被发送时
- RPC结束时(包含成功或失败)
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(streaming RPC)
使用 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)
}
如果不想立即发送header
,也可以使用ServerStream.SetHeader()
。ServerStream.SetHeader()
可以被多次调用,在如下时机会把多个metadata
合并发送出去
- 调用
ServerStream.SendHeader()
- 第一个响应被发送时
- RPC结束时(包含成功或失败)
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)
}
Client 接收 Metadata
🌲 普通RPC(unary RPC)
普通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(streaming RPC)
流式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()
Header
和Trailer
区别
根本区别:发送的时机不同!
✨ headers
会在下面三种场景下被发送
SendHeader()
被调用时(包含grpc.SendHeader
和stream.SendHeader
)- 第一个响应被发送时
- RPC结束时(包含成功或失败)
✨ trailer
会在rpc返回的时候,即这个请求结束的时候被发送
差异在流式RPC(streaming RPC)中比较明显:
因为trailer
是在服务端发送完请求之后才发送的,所以client获取trailer
的时候需要在stream.CloseAndRecv
或者stream.Recv
返回非nil错误 (包含 io.EOF)之后
如果stream.CloseAndRecv
之前调用stream.Trailer()
获取的是空
stream, err := client.SomeStreamingRPC(ctx)
// retrieve header
header, err := stream.Header()
// retrieve trailer
// `trailer`会在rpc返回的时候,即这个请求结束的时候被发送
// 因此此时调用`stream.Trailer()`获取的是空
trailer := stream.Trailer()
stream.CloseAndRecv()
// retrieve trailer
// `trailer`会在rpc返回的时候,即这个请求结束的时候被发送
// 因此此时调用`stream.Trailer()`才可以获取到值
trailer := stream.Trailer()
使用场景
既然我们把metadata
类比成HTTP Header
,那么metadata
的使用场景也可以借鉴HTTP
的Header
。如传递用户token
进行用户认证,传递trace
进行链路追踪等
拦截器中的metadata
在拦截器中,我们不但可以获取或修改接收到的metadata
,甚至还可以截取并修改要发送出去的metadata
还记得拦截器如何实现么?如果已经忘了快快回顾一下吧:
🌰 举个例子:
我们在客户端拦截器中从要发送给服务端的metadata
中读取一个时间戳字段,如果没有则补充这个时间戳字段
注意这里用到了一个上文没有提到的FromOutgoingContext(ctx)
函数
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 main() {
conn, err := grpc.Dial("127.0.0.1:8009",
grpc.WithInsecure(),
grpc.WithChainUnaryInterceptor(
orderUnaryClientInterceptor,
),
)
if err != nil {
panic(err)
}
c := pb.NewOrderManagementClient(conn)
ctx = metadata.AppendToOutgoingContext(context.Background(), "time",
"raw"+strconv.FormatInt(time.Now().UnixNano(), 10))
// 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 := c.AddOrder(ctx, &order)
if err != nil {
panic(err)
}
}
以上的思路在server同样适用。基于以上原理我们可以实现链路追踪、用户认证等功能
错误信息
还记得错误处理一文中留下的问题么:gRPC 中如何传递错误消息Status
的呢?没错!也是使用的metadata
或者说http2.0
的header
。Status
的三种信息分别使用了三个header
头
Grpc-Status
: 传递Status
的code
Grpc-Message
: 传递Status
的message
Grpc-Status-Details-Bin
: 传递Status
的details
func (ht *serverHandlerTransport) WriteStatus(s *Stream, st *status.Status) error {
// ...
h := ht.rw.Header()
h.Set("Grpc-Status", fmt.Sprintf("%d", st.Code()))
if m := st.Message(); m != "" {
h.Set("Grpc-Message", encodeGrpcMessage(m))
}
if p := st.Proto(); p != nil && len(p.Details) > 0 {
stBytes, err := proto.Marshal(p)
if err != nil {
// TODO: return error instead, when callers are able to handle it.
panic(err)
}
h.Set("Grpc-Status-Details-Bin", encodeBinHeader(stBytes))
}
// ...
}
总结
一张图总结下整个metadata
的使用方法(公众号发送:metadata总结获取高清原文件)
推荐阅读
参考资料
✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。