元数据是一个侧通道,允许客户端和服务器相互提供与 RPC 相关的信息。gRPC 元数据是 key-value 对组成的数据,用于初始化和结束gRPC请求和响应。它通常提供附加的信息对于调用,例如认证,链路信息,自定义请求头。gRPC的元数据可以同时在客户端和服务端设置发送和接受。
发送和接收 Metadata(客户端)
Sending Metadata
// create a new context with some metadata
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")
// later, add some more metadata to the context (e.g. in an interceptor)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")
// make unary RPC
response, err := client.SomeRPC(ctx, someRequest)
// or make streaming RPC
stream, err := client.SomeStreamingRPC(ctx)
也可以使用以下的方式
// create a new context with some metadata
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)
// later, add some more metadata to the context (e.g. in an interceptor)
send, _ := metadata.FromOutgoingContext(ctx)
newMD := metadata.Pairs("k3", "v3")
ctx = metadata.NewOutgoingContext(ctx, metadata.Join(send, newMD))
// make unary RPC
response, err := client.SomeRPC(ctx, someRequest)
// or make streaming RPC
stream, err := client.SomeStreamingRPC(ctx)
Receiving Metadata
Unary call
var header, trailer metadata.MD // variable to store header and trailer
r, err := client.SomeRPC(
ctx,
someRequest,
grpc.Header(&header), // will retrieve header
grpc.Trailer(&trailer), // will retrieve trailer
)
// do something with header and trailer
Streaming call
stream, err := client.SomeStreamingRPC(ctx)
// retrieve header
header, err := stream.Header()
// retrieve trailer
trailer := stream.Trailer()
发送和接收 Metadata(服务端)
Receiving metadata
Unary call
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
md, ok := metadata.FromIncomingContext(ctx)
// do something with metadata
}
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
}
Sending metadata
Unary call
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
// create and send header
header := metadata.Pairs("header-key", "val")
grpc.SendHeader(ctx, header)
// create and set trailer
trailer := metadata.Pairs("trailer-key", "val")
grpc.SetTrailer(ctx, trailer)
}
Streaming call
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)
}
完整示例
开发环境:
golang 1.20.1
protoc libprotoc 3.20.3
echo.proto
syntax = "proto3";
package echo;
option go_package = "com/example/echo";
service EchoService{
rpc UnaryEcho(EchoRequest) returns(EchoResponse){}
rpc ServerStreamingEcho(EchoRequest) returns(stream EchoResponse){}
rpc ClientStreamingEcho(stream EchoRequest) returns(EchoResponse){}
rpc BidirectionalStreamingEcho(stream EchoRequest) returns(stream EchoResponse){}
}
message EchoRequest{
string message = 1;
}
message EchoResponse{
string message = 1;
}
服务端 server.go
package main
import (
"example.com/com/example/echo"
"google.golang.org/grpc"
"log"
"net"
)
func main() {
server := grpc.NewServer()
echo.RegisterEchoServiceServer(server, &echo.EchoService{})
listen, err := net.Listen("tcp", ":8090")
if err != nil {
log.Print(err.Error())
}
log.Print("listen on port:8090 >>>>")
if er := server.Serve(listen); er != nil {
log.Print(er.Error())
}
}
客户端 client.go
package main
import (
"context"
"example.com/com/example/echo"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"io"
"log"
"strconv"
"time"
)
func callUnaryEchoWithMetadata(client echo.EchoServiceClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 设置 metadata
md := metadata.New(map[string]string{"request-id": strconv.FormatInt(time.Now().Unix(), 10)}) // metadata.Pairs()
ctx = metadata.NewOutgoingContext(ctx, md)
// 也可以这样设置 grpc.SetHeader(ctx, md)
var header metadata.MD
resp, err := client.UnaryEcho(ctx, &echo.EchoRequest{
Message: "hello",
}, grpc.Header(&header))
for k, v := range header {
log.Println(k, "=", v)
}
if err != nil {
log.Print(err.Error())
return
}
log.Print("Unary Echo:", resp.Message)
}
func callBidiStreamEchoWithMetadata(client echo.EchoServiceClient) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 设置 metadata
ctx = metadata.AppendToOutgoingContext(ctx, "stream-request-id", strconv.FormatInt(time.Now().Unix(), 10))
stream, err := client.BidirectionalStreamingEcho(ctx)
if err != nil {
return
}
for i := 0; i < 5; i++ {
if err := stream.Send(&echo.EchoRequest{Message: fmt.Sprintf("Request %d", i+1)}); err != nil {
log.Fatalf("failed to send request due to error: %v", err)
}
}
stream.CloseSend()
// 接受服务端的 stream metadata
if md, err := stream.Header(); err == nil {
log.Println("stream metadata:")
for k, v := range md {
log.Println(k, "::", v)
}
}
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("failed to receive response due to error: %v", err)
}
fmt.Println("BidiStreaming Echo: ", resp.Message)
}
}
func main() {
conn, err := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
defer conn.Close()
client := echo.NewEchoServiceClient(conn)
callUnaryEchoWithMetadata(client)
// callBidiStreamEchoWithMetadata(client)
}
echo.go
package echo
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"io"
"log"
)
type EchoService struct {
EchoServiceServer
}
func (s *EchoService) UnaryEcho(ctx context.Context, req *EchoRequest) (*EchoResponse, error) {
log.Println("unary echoing message=", req.Message)
// 读取 metadata
if md, ok := metadata.FromIncomingContext(ctx); ok {
log.Println("metadata map:")
for k, v := range md {
log.Println(k, " :: ", v)
}
grpc.SendHeader(ctx, metadata.Pairs("request-id", md["request-id"][0], "type", "unary"))
}
return &EchoResponse{
Message: req.Message,
}, nil
}
func (s *EchoService) BidirectionalStreamingEcho(stream EchoService_BidirectionalStreamingEchoServer) error {
// 接受metadata
md, ok := metadata.FromIncomingContext(stream.Context())
if ok {
log.Println("server metadata:")
for k, v := range md {
log.Println(k, "::", v)
}
}
for {
in, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
fmt.Printf("server: error receiving from stream: %v\n", err)
return err
}
fmt.Printf("bidi echoing message %q\n", in.Message)
// 设置响应的 metadata
stream.SetHeader(metadata.Pairs("stream-request-id", md["stream-request-id"][0], "type", "stream"))
stream.Send(&EchoResponse{Message: in.Message})
}
}
每次在发送和接受的数据的时候,增加或者解析 Metadata 数据的时候非常麻烦,导致很多冗余的代码,所以我们可以将 Metadata 的数据放到 Interceptor 中处理。
客户端拦截代码,服务端类似
package main
import (
"context"
"example.com/com/example/echo"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"log"
"strconv"
"time"
)
func callUnaryEcho(client echo.EchoServiceClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := client.UnaryEcho(ctx, &echo.EchoRequest{
Message: "hello",
})
if err != nil {
log.Print(err.Error())
return
}
log.Print("Unary Echo:", resp.Message)
}
func echoUnaryClientInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
ctx = metadata.AppendToOutgoingContext(ctx, "request-id", strconv.FormatInt(time.Now().Unix(), 10))
var responseHeader metadata.MD
err := invoker(ctx, method, req, reply, cc, grpc.Header(&responseHeader))
end := time.Now()
if responseHeader != nil {
for k, v := range responseHeader {
log.Println(k, "=", v)
}
log.Printf("Client metadata interceptor cost=%v\n", end.Sub(start).Milliseconds())
}
return err
}
func main() {
conn, err := grpc.Dial("127.0.0.1:8090", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainUnaryInterceptor(echoUnaryClientInterceptor))
if err != nil {
panic(err)
}
defer conn.Close()
client := echo.NewEchoServiceClient(conn)
callUnaryEcho(client)
}