三、Golang gRPC 拦截器

40 阅读3分钟

gRPC 提供了一些简单的 API 在 ClientConn/Server 实现和安装拦截器,拦截器拦截每一次 RPC 调用的执行。 用户可以使用拦截器用于日志、认证、指标/监控等。

image.png

在 gRPC 中,拦截器分为 2 种,一种是 unary interceptor ,用于拦截 unary RPC 调用。另一种是 stream interceptor ,用于处理 stream RPC 调用。 每一个客户端和服务端都有 unary interceptorstream interceptor ,因此在 gRPC 中4种类型的拦截器。

客户端

Unary Interceptor

UnaryClientInterceptor是一个客户端类型的拦截器,方法签名为

func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

实现 unary interceptor 通常分为三部分,pre-processing, invoke RPC method 和 post-processing。

对于安装 unary interceptor 在 ClientConn 上,配置DailDialOption WithUnaryInterceptor

Stream Interceptor

StreamClientInterceptor 是一个客户端流类型的拦截器,方法签名为

func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

一个流拦截器的实现通常包含处理前和流操作拦截。

对于安装 unary interceptor 在 ClientConn 上,配置 DailDialOption WithStreamInterceptor

服务端

服务端和客户端拦截器有些类似,稍微有一些不同。

Unary Interceptor

UnaryServerInterceptor 是服务端一种拦截器,方法签名为

func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

对于安装 unary interceptor 在服务端,配置 NewServerNewServer

Stream Interceptor

StreamServerInterceptor 是一种服务端流类型的拦截器,方法签名为

func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

对于安装 stream interceptor 在服务端,配置 NewServerNewServer

示例代码

开发环境:
golang 1.20.1
protoc libprotoc 3.20.3

代码目录结构

└──com
    └── example
        ├── echo
        │   ├── echo.go
        │   ├── echo.pb.go
        │   ├── echo.proto
        │   └── echo_grpc.pb.go
        └──interceptor
            ├── client.go
            └── server.go

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;
}

业务 echo.go

package echo

import (
 "context"
 "fmt"
 "io"
 "log"
)

type EchoService struct {
 EchoServiceServer
}

func (s *EchoService) UnaryEcho(ctx context.Context, req *EchoRequest) (*EchoResponse, error) {
 log.Print("unary echoing message=", req.Message)
 return &EchoResponse{
  Message: req.Message,
 }, nil
}

func (s *EchoService) BidirectionalStreamingEcho(stream EchoService_BidirectionalStreamingEchoServer) error {
 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)
  stream.Send(&EchoResponse{Message: in.Message})
 }
}

服务端(server.go)

package main

import (
 "context"
 "example.com/com/example/echo"
 "google.golang.org/grpc"
 "log"
 "net"
 "time"
)
// 服务端 unary 拦截器
func echoUnaryServerInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
 start := time.Now()
 resp, err = handler(ctx, req)
 end := time.Now()
 log.Print("RPC unary server interceptor:", end.Sub(start))
 return resp, err
}

// 服务端 stream 拦截器
func echoStreamServerInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
 log.Print("RPC stream server interceptor:", info.FullMethod)
 err := handler(srv, ss)
 if err != nil {
  log.Print(err)
 }
 return err
}

func main() {

 server := grpc.NewServer(grpc.ChainUnaryInterceptor(echoUnaryServerInterceptor), grpc.ChainStreamInterceptor(echoStreamServerInterceptor))
 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"
 "io"
 "log"
 "time"
)
// 客户端 unary 拦截器
func echoUnaryClientInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
 start := time.Now()
 err := invoker(ctx, method, req, reply, cc, opts...)
 end := time.Now()
 log.Print("RPC unary client interceptor:", method, "\n start time:", start, "\n end time:", end, "\n err:", err)
 return err
}

// 客户端 stream 拦截器
func echoStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
 log.Print("RPC stream client interceptor :", method)
 s, err := streamer(ctx, desc, cc, method, opts...)
 if err != nil {
  return nil, err
 }
 return s, nil
}

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 callBidiStreamEcho(client echo.EchoServiceClient) {
 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 defer cancel()
 c, err := client.BidirectionalStreamingEcho(ctx)
 if err != nil {
  return
 }
 for i := 0; i < 5; i++ {
  if err := c.Send(&echo.EchoRequest{Message: fmt.Sprintf("Request %d", i+1)}); err != nil {
   log.Fatalf("failed to send request due to error: %v", err)
  }
 }
 c.CloseSend()
 for {
  resp, err := c.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()),
  grpc.WithChainUnaryInterceptor(echoUnaryClientInterceptor),
  grpc.WithChainStreamInterceptor(echoStreamInterceptor))
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 client := echo.NewEchoServiceClient(conn)
 callUnaryEcho(client)
 callBidiStreamEcho(client)
}

总结,对 gRPC 客户端和服务端的 unary 和 stream 的拦截器的了解以及代码实现。

参考:
gRPC Github Doc
gRPC Web Interceptor Blog