五、Golang gRPC Metadata

30 阅读3分钟

元数据是一个侧通道,允许客户端和服务器相互提供与 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)
}

参考:
gRPC Metadata
gRPC Metadata Github