gRPC 的拦截器


简介


在构建 gRPC 应用程序时,无论是客户端应用程序,还是服务器端应用程序,在远程方法执行之前或之后,都可能需要执行一些通用逻辑。

gRPC 提供了简单的 API,用来在客户端和服务器端的 gRPC 应用程序中实现并安装拦截器。它是 gRPC 核心扩展机制之一,在一些使用场景中(如日志、身份验证、授权、性能度量指标、跟踪以及其他一些自定义需求),拦截器拦截每个 RPC 调用的执行,可以使用拦截器进行日志记录、身份验证/授权、指标收集以及许多其他可以跨 RPC 共享的功能。

在 gRPC 应用程序中,拦截器根据拦截的 RPC 调用类型可以分为以下的两大类:

  • 第一个是一元拦截器(unary interceptor),它拦截一元 RPC 的调用;

  • 第二个是流拦截器(streaming interceptor),它处理流式 RPC 的调用,客户端和服务端都可使用普通拦截器和流拦截器。


服务端拦截器

当客户端调用 gRPC 服务的远程方法时,通过使用服务器端拦截器,可以在执行远程方法之前,执行一个通用的逻辑。

在所开发的任意 gRPC 服务器端,都可以插入一个或多个拦截器。如果希望向 gRPC 服务中插入新服务器端拦截器,则可以实现该拦截器并在创建 gRPC 服务器端时将其注册进来,如下图所示:

在服务器端,一元拦截器拦截一元 RPC,流拦截器则拦截流 RPC ,以下是具体的说明:

(1)gRPC 服务端一元拦截器

服务器端一元拦截器会拦截 gRPC 服务器所处理的所有一元 RPC ,如果想在服务器端拦截 gRPC 服务的一元 RPC,需要为 gRPC 服务器端实现一元拦截器。首先要实现 UnaryServerInterceptor 类型的函数,并在创建 gRPC 服务器端时将函数注册进来,该函数是用于服务器端一元拦截器的类型,它具有以下签名:

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

若要为服务端安装一元拦截器,需使用 UnaryInterceptor 中的 ServerOption 配置 NewServer。

服务器端一元拦截器的实现通常可以分为以下的前置处理、调用 RPC 方法以及后置处理三个阶段:

  • 前置处理阶段。在前置处理阶段,在调用预期的 RPC 远程方法之前执行,用户可以通过检查传入的参数来获取关于当前 RPC 的信息,比如 RPC 上下文、RPC 请求和服务器端信息,甚至可以在预处理阶段修改 RPC ;

  • 调用阶段。在这个阶段中,需要调用 gRPC UnaryHandler 来触发 RPC 方法,在调用 RPC 之后,就进入后置处理阶段。这意味着,RPC 响应要流经后置处理阶段;

  • 后置阶段。在这个阶段中,可以按需处理返回的响应和错误。当后置处理阶段完成之后,需要以拦截器函数返回参数的形式将消息和错误返回。如果不需要后置处理器,那么可以直接返回 handler 调用(handler(ctx, req)) 。

具体的编程使用方法参考如下关键部分的程序代码:

// 服务器端一元拦截器
func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {

		// 前置处理逻辑
		// 前置处理阶段:可以在调用对应的 RPC 之前拦截消息(如通过检查传入的参数,获取关于当前 RPC 的信息)
		log.Println("======= [Server Interceptor] ", info.FullMethod) 

		// 调用 handler 完成一元 RPC 的正常执行
		// 通过 UnaryHandler 调用 RPC 方法
		m, err := handler(ctx, req) 

		// 后置处理逻辑
		// 后置处理阶段:可以在这里处理 RPC 响应
		log.Printf(" Post Proc Message : %s", m) 
		// 将 RPC 响应发送回去
		return m, err 
}

...

func main() {
		...
		// 在服务器端注册拦截器,使用 gRPC 服务器端注册一元拦截器
		s := grpc.NewServer(grpc.UnaryInterceptor(orderUnaryServerInterceptor)) 
		...

(2)gRPC 服务端流拦截器

服务器端流拦截器会拦截 gRPC 服务器所处理的所有流 RPC ,如果想在服务器端拦截 gRPC 服务的流式 RPC,需要为 gRPC 服务器端实现流拦截器。首先要实现 StreamServerInterceptor 类型的函数,并在创建 gRPC 服务器端时将函数注册进来,该函数是用于服务器端一元拦截器的类型,它具有以下签名:

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

若要为服务端安装流拦截器,请使用 StreamInterceptor 中的 ServerOption 来配置 NewServer 。

流拦截器分为以下的前置处理阶段和流操作拦截阶段:

  • 前置处理阶段。与一元拦截器类似,在前置处理阶段,可以在流 RPC 进入服务实现之前对其进行拦截。

  • 流操作拦截阶段。在前置处理阶段之后,则可以调用 StreamHandler 来完成远程方法的 RPC 执行,而且通过已实现 grpc.ServerStream 接口的包装器流接口,可以拦截流 RPC 的消息。在通过 handler(srv, newWrappedStream(ss)) 方法调用 grpc.StreamHandler 时,可以将这个包装器结构传递进来。grpc.ServerStream 的包装器可以拦截 gRPC 服务发送或接收到的数据,它实现了 SendMsg 函数和 RecvMsg 函数,这两个函数分别会在服务发送和接收 RPC 流消息的时候被调用。

具体的编程使用方法参考如下关键部分的程序代码:

// 服务器端流拦截器
// wrappedStream 包装嵌入的 grpc.ServerStream
// 并拦截对 RecvMsg 函数和 SendMsg 函数的调用
type wrappedStream struct { 
		grpc.ServerStream
}

// 实现包装器的 RecvMsg 函数,来处理流 RPC 所接收到的消息
func (w *wrappedStream) RecvMsg(m interface{}) error {
		log.Printf("====== [Server Stream Interceptor Wrapper] " + "Receive a message (Type: %T) at %s", m, time.Now().Format(time.RFC3339))
		return w.ServerStream.RecvMsg(m)
}

// 实现包装器的 SendMsg 函数,来处理流 RPC 所发送的消息
func (w *wrappedStream) SendMsg(m interface{}) error {
		log.Printf("====== [Server Stream Interceptor Wrapper] " + "Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
		return w.ServerStream.SendMsg(m)
}

// 创建新包装器流的实例
func newWrappedStream(s grpc.ServerStream) grpc.ServerStream {
		return &wrappedStream{s}
}

// 流拦截器的实现
func ServerStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		log.Println("====== [Server Stream Interceptor] ", info.FullMethod) 
		// 使用包装器流调用流 RPC
		err := handler(srv, newWrappedStream(ss)) 
		if err != nil {
				log.Printf("RPC failed with error %v", err)
		}
		return err
}

...
		// 注册拦截器
		s := grpc.NewServer(
		grpc.StreamInterceptor(orderServerStreamInterceptor)) 
...
}

客户端拦截器

当客户端发起 RPC 来触发 gRPC 服务的远程方法时,可以在客户端拦截这些 RPC,借助客户端拦截器,可以拦截一元 RPC 和流 RPC ,如下图所示:

在客户端,一元拦截器拦截一元 RPC,流拦截器则拦截流 RPC ,以下是具体的说明:

(1)gRPC 客户端一元拦截器

客户端一元拦截器用于拦截一元 RPC 客户端的调用,UnaryClientInterceptor 是客户端一元拦截器的类型,函数签名如下:

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

与前面介绍的服务器端一元拦截器一样,客户端一元拦截器也有以下的处理阶段:

  • 前置阶段。在前置处理阶段,可以在调用远程方法之前拦截 RPC,还可以通过检查传入的参数来访问关于当前 RPC 的信息(如 RPC 的上下文、方法字符串、要发送的请求以及 CallOption 配置),甚至可以在原始的 RPC 发送至服务器端应用程序之前,对其进行修改。随后,借助 UnaryInvoker 参数,可以调用实际的一元 RPC ;

  • 后置阶段。在后置处理阶段,可以访问 RPC 的响应结果或错误结果。

若要在 ClientConn 上安装一元拦截器,需使用 DialOptionWithUnaryInterceptor 的 DialOption 配置 Dial 。

具体的编程使用方法参考如下关键部分的程序代码:

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

		// 前置处理阶段,前置处理阶段能够在 RPC 请求发送至服务器端之前访问它
		log.Println("Method : " + method) 

		// 通过 UnaryInvoker 调用 RPC 远程方法
		err := invoker(ctx, method, req, reply, cc, opts...) 

		// 后置处理阶段,可以在这里处理响应结果或错误结果
		log.Println(reply) 

		return err 
}

...

func main() {
		// 建立到服务器端的连接,通过传入一元拦截器作为 grpc.Dial 的选项,建立到服务器端的连接
		// 注册拦截器函数通过使用 grpc.WithUnaryInterceptor 在 grpc.Dial 操作中实现 
		conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithUnaryInterceptor(orderUnaryClientInterceptor)) 

		...
}

(2)gRPC 客户端流拦截器

客户端流拦截器会拦截 gRPC 客户端所处理的所有流 RPC。客户端流拦截器的实现与服务器端流拦截器的实现非常相似,StreamClientInterceptor 是客户端流拦截器的类型,其函数类型签名如下所示:

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

客户端流拦截器实现包括以下的前置处理和流操作拦截阶段:

  • 前置处理阶段。类似于上面的一元拦截器;

  • 流操作拦截阶段。流拦截器并没有事后进行 RPC 方法调用和后处理,而是拦截了用户在流上的操作。首先,拦截器调用传入的 streamer 以获取 ClientStream ,然后包装 ClientStream 并用拦截逻辑重载其方法,最后拦截器将包装好的 ClientStream 返回给用户进行操作。

若要为 ClientConn 安装流拦截器,需要使用 WithStreamInterceptor 的 DialOption 配置 Dial。

具体的编程使用方法参考如下关键部分的程序代码:

func clientStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
		// 前置处理阶段能够在将 RPC 请求发送至服务器端之前访问它
		log.Println("======= [Client Interceptor] ", method) 
		// 调用传入的 streamer 来获取 ClientStream
		s, err := streamer(ctx, desc, cc, method, opts...) 
  		if err != nil {
				return nil, err
		}
		// 包装 ClientStream,使用拦截逻辑重载其方法并返回给客户端应用程序
		return newWrappedStream(s), nil 
}

// grpc.ClientStream 的包装器流
type wrappedStream struct { 
		grpc.ClientStream
}

// 拦截流 RPC 所接收消息的函数
func (w *wrappedStream) RecvMsg(m interface{}) error { 
		log.Printf("====== [Client Stream Interceptor] " + "Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
		return w.ClientStream.RecvMsg(m)
}

// 拦截流 RPC 所发送消息的函数
func (w *wrappedStream) SendMsg(m interface{}) error { 
		log.Printf("====== [Client Stream Interceptor] " + "Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
		return w.ClientStream.SendMsg(m)
}

// 注册流拦截器
func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
		return &wrappedStream{s}
}

...

func main() {

		// 建立到服务器端的连接
		conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithStreamInterceptor(clientStreamInterceptor)) 

		...
}

流操作拦截是通过流的包装器实现完成的,该实现中必须实现包装 grpc.ClientStream 的新结构。这里实现了两个包装流的函数(即 RecvMsg 函数和 SendMsg 函数),分别用来拦截客户端接收及发送的流消息。拦截器的注册和一元拦截器是一样的,都是通过 grpc.Dial 操作完成的。


拦截器程序示例


一元拦截器

(1)在任意目录下,创建 serverclient 目录存放服务端和客户端文件,proto 目录用于编写 IDL 的 interceptor.proto 文件,具体的目录结构如下所示:

UnaryInterceptor
├── client
│   └── proto
│       └── interceptor.proto
└── server
    └── proto
        └── interceptor.proto

(2)在 proto 文件夹下的 interceptor.proto 文件中,写入如下内容:

syntax = "proto3"; // 版本声明,使用 Protocol Buffers v3 版本

option go_package = "../proto";  // 指定生成的 Go 代码在项目中的导入路径

package interceptor; // 包名

// 定义服务
service Greeter {
    	// SayHello 方法
    	rpc SayHello (HelloRequest) returns (HelloResponse) {}

    	rpc SayHelloAgain (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    	string name = 1;
}

// 响应消息
message HelloResponse {
    	string reply = 1;
}

为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

UnaryInterceptor
├── client
│   └── proto
│       ├── interceptor_grpc.pb.go
│       ├── interceptor.pb.go
│       └── interceptor.proto
└── server
    └── proto
        ├── interceptor_grpc.pb.go
        ├── interceptor.pb.go
        └── interceptor.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序实现服务器一元拦截器功能,该程序的具体代码如下:

package main

import (
        "context"
        "fmt"
        pb "server/proto"
        "net"
        "log"
        "google.golang.org/grpc"
)

// hello server

type server struct {
        pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
        return &pb.HelloResponse{Reply: "Hello " + in.Name}, nil
}

func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
        return &pb.HelloResponse{Reply: "Hello " + in.Name + " again!"}, nil
}

// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        // authentication (token verification)
        fmt.Println("-------------------\t")
        log.Println("===> 服务端前置处理逻辑开始执行!")

        m, err := handler(ctx, req)

        fmt.Println("-------------------\t")
        log.Println("===> 服务端后置处理逻辑开始执行!\n", m)

        return m, err
}


func main() {
        // 监听本地的 8972 端口
        lis, err := net.Listen("tcp", ":8972")
        if err != nil {
                fmt.Printf("failed to listen: %v", err)
                return
        }
        s := grpc.NewServer(
                grpc.UnaryInterceptor(unaryInterceptor),
        )                  

        // 创建 gRPC 服务器
        pb.RegisterGreeterServer(s, &server{}) // 在 gRPC 服务端注册服务
        // 启动服务
        err = s.Serve(lis)
        if err != nil {
                fmt.Printf("failed to serve: %v", err)
        }
}

(4)在 client 目录下初始化项目( go mod init client ),编写 Client 端程序实现客户端一元拦截器功能,该程序的具体代码如下:

package main

import (
        "context"
        "flag"
        "log"
        "time"
        "fmt"
        pb "client/proto"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

// hello_client

const (
        defaultName = "cqupthao"
)

var (
        addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
        name = flag.String("name", defaultName, "Name to greet")
)


// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {

        fmt.Println("------------------\t")
        log.Println("===> 客户端前置处逻辑开始执行!")

        err := invoker(ctx, method, req, reply, cc, opts...)

        fmt.Println("------------------\t")
        log.Println("===> 客户端后置处理逻辑开始执行!")
        log.Println(reply)

        return err

}

func main() {
        flag.Parse()
        // 连接到 server 端,此处禁用安全传输
        conn, err := grpc.Dial(*addr, 
                grpc.WithTransportCredentials(insecure.NewCredentials()),
                grpc.WithUnaryInterceptor(unaryInterceptor),
        )
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        // 执行 RPC 调用并打印收到的响应数据
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetReply())

        r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: *name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetReply())
}

(5)执行 Server 端和 Client 端的程序,分别输出如下的结果:

// Server
-------------------
2023/02/25 23:14:40 ===> 服务端前置处理逻辑开始执行!
-------------------
2023/02/25 23:14:40 ===> 服务端后置处理逻辑开始执行!
 reply:"Hello cqupthao"
-------------------
2023/02/25 23:14:40 ===> 服务端前置处理逻辑开始执行!
-------------------
2023/02/25 23:14:40 ===> 服务端后置处理逻辑开始执行!
 reply:"Hello cqupthao again!"
// Client
------------------
2023/02/25 23:14:40 ===> 客户端前置处逻辑开始执行!
------------------
2023/02/25 23:14:40 ===> 客户端后置处理逻辑开始执行!
2023/02/25 23:14:40 reply:"Hello cqupthao"
2023/02/25 23:14:40 Greeting: Hello cqupthao
------------------
2023/02/25 23:14:40 ===> 客户端前置处逻辑开始执行!
------------------
2023/02/25 23:14:40 ===> 客户端后置处理逻辑开始执行!
2023/02/25 23:14:40 reply:"Hello cqupthao again!"
2023/02/25 23:14:40 Greeting: Hello cqupthao again!

流拦截器

(1)在任意目录下,创建 serverclient 目录存放服务端和客户端文件,创建 cert 目录存放证书文件,创建 proto 目录用于编写 IDL 的 interceptor.proto 文件,具体的目录结构如下所示:

StreamInterceptor
├── client
│   ├── cert
│   └── proto
│       └── interceptor.proto
└── server
    ├── cert
    └── proto
        └── interceptor.proto

(2)在 proto 文件夹下的 interceptor.proto 文件中,写入如下内容:

syntax = "proto3";
package proto;

option go_package = "../proto";

service StreamService {
    	rpc Route(stream StreamRequest) returns (stream StreamResponse) {};
}
message StreamPoint {
  		string name = 1;
  		int32 value = 2;
}
message StreamRequest {
  		StreamPoint pt = 1;
}
message StreamResponse {
  		StreamPoint pt = 1;
}

移动证书文件到 cert 目录下,为服务端和客户端生成 gRPC 源代码程序,在 proto 目录下执行以下的命令:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

正确生成后的目录结构如下所示:

ch06
├── client
│   ├── cert
│   │   ├── ca.crt
│   │   ├── server.key
│   │   └── server.pem
│   └── proto
│       ├── interceptor_grpc.pb.go
│       ├── interceptor.pb.go
│       └── interceptor.proto
└── server
    ├── cert
    │   ├── ca.crt
    │   ├── server.key
    │   └── server.pem
    └── proto
        ├── interceptor_grpc.pb.go
        ├── interceptor.pb.go
        └── interceptor.proto

(3)在 server 目录下初始化项目( go mod init server ),编写 Server 端程序实现服务端流拦截器功能,该程序的具体代码如下:

package main

import (
        "log"
        "net"
        "crypto/tls"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc"
        "io"
        pb "server/proto"
        "google.golang.org/grpc/status"
        "fmt"
        "time"
        "strings"
)

type StreamService struct{
        pb.UnimplementedStreamServiceServer
}

const (
        PORT = "9002"
)

var (
        crtFile = "cert/server.pem"
        keyFile = "cert/server.key"
        errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata!")
        errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token!")
)

func (s *StreamService) Route(stream pb.StreamService_RouteServer) error {
        n := 0
        for {
                err := stream.Send(&pb.StreamResponse{
                        Pt: &pb.StreamPoint{
                        Name:  "gPRC Stream Client: Route",
                        Value: int32(n),
                        },
                })
                if err != nil {
                        return err
                }
                r, err := stream.Recv()
                if err == io.EOF {
                        return nil
                }
                if err != nil {
                        return err
                }
                n++
                log.Printf("stream.Recv pt.name: %s, pt.value: %d", r.Pt.Name, r.Pt.Value)
        }
        return nil
}

type wrappedStream struct {
        grpc.ServerStream
}

func (w *wrappedStream) RecvMsg(m interface{}) error {
        log.Println("====> [Server Stream Interceptor Wrapper] " + "Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
        return w.ServerStream.RecvMsg(m)
}

func (w *wrappedStream) SendMsg(m interface{}) error {
        log.Println("====> [Server Stream interceptor Wrapper] " + "Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
        return w.ServerStream.SendMsg(m)
}

func newWrappedStream(s grpc.ServerStream) grpc.ServerStream {
        return &wrappedStream{s}
}

// streamInterceptor 服务端流拦截器
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
        // authentication (token verification)
        log.Println("====> [Server Stream Interceptor Wrapper] ", info.FullMethod)
        md, ok := metadata.FromIncomingContext(ss.Context())
        if !ok {
                return errMissingMetadata
        }
        if !valid(md["authorization"]) {
                return errInvalidToken
        }

        err := handler(srv, newWrappedStream(ss))
        if err != nil {
                fmt.Printf("RPC failed with error %v\n", err)
        }
        return err
}

// valid 校验认证信息.
func valid(authorization []string) bool {
        if len(authorization) < 1 {
                return false
        }
        token := strings.TrimPrefix(authorization[0], "Bearer ")
        // 执行token认证的逻辑
        // 这里是为了演示方便简单判断token是否与"some-secret-token"相等
        return token == "some-secret-token"
}

func main() {
        cert, err := tls.LoadX509KeyPair(crtFile, keyFile)
        if err != nil {
                log.Println("failed to load key pair: %s", err)
        }
        opts := []grpc.ServerOption{
                grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
                grpc.StreamInterceptor(streamInterceptor),
        }
        server := grpc.NewServer(opts...)
        pb.RegisterStreamServiceServer(server, &StreamService{})
    
        lis, err := net.Listen("tcp", ":"+PORT)
        if err != nil {
                log.Fatalf("net.Listen err: %v", err)
        }
    
        server.Serve(lis)
        if err != nil {
                log.Println("failed to serve: %v ", err)
                return
        }
}

(4)在 client 目录下初始化项目( go mod init client ),编写 Client 端程序实现客户端流拦截器功能,该程序的具体代码如下:

package main

import (
        "log"
        "time"
        "context"
        "google.golang.org/grpc"
        pb "client/proto"
        "io"
        "google.golang.org/grpc/credentials"
        "golang.org/x/oauth2"
        "google.golang.org/grpc/credentials/oauth"
)
const (
        PORT = "9002"
)

var (
        crtFile = "cert/server.pem"
        hostname = "*.cqupthao.com"
)

func printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {
        stream, err := client.Route(context.Background())
        if err != nil {
                return err
        }
        for n := 0; n < 2; n++ {
                err = stream.Send(r)
                if err != nil {
                        return err
                }
                resp, err := stream.Recv()
                if err == io.EOF {
                        break
                }
                if err != nil {
                        return err
                }
        log.Printf("resp: cqupthao.name: %s, pt.value: %d", resp.Pt.Name, resp.Pt.Value)
        }
        stream.CloseSend()
        return nil
}

type wrappedStream struct {
        grpc.ClientStream
}

func (w *wrappedStream) RecvMsg(m interface{}) error {
        log.Println("====> [Client Stream Interceptor] " + "Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
        return w.ClientStream.RecvMsg(m)
}

func (w *wrappedStream) SendMsg(m interface{}) error {
        log.Println("====> [Client Stream interceptor] " + "Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
        return w.ClientStream.SendMsg(m)
}

func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
        return &wrappedStream{s}
}

// streamInterceptor 客户端流式拦截器
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
        var credsConfigured bool
        for _, o := range opts {
                _, ok := o.(*grpc.PerRPCCredsCallOption)
                if ok {
                        credsConfigured = true
                        break
                }
        }
        if !credsConfigured {
                opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(fetchToken())))
        }
        log.Println("====> [Client Stream Interceptor] ", method)
        s, err := streamer(ctx, desc, cc, method, opts...)
        if err != nil {
                return nil, err
        }
        return newWrappedStream(s), nil
}

func main() {
        //auth := oauth.NewOauthAccess(fetchToken())
        creds, err := credentials.NewClientTLSFromFile(crtFile, hostname)
        if err != nil {
                log.Fatalf("failed to load credentials: %v ", err)
        }

        opts := []grpc.DialOption{
                //grpc.WithPerRPCCredentials(auth),
                grpc.WithTransportCredentials(creds),
                grpc.WithStreamInterceptor(streamInterceptor),
        }
        conn, err := grpc.Dial(":"+PORT, opts...)
        if err != nil {
                log.Fatalf("grpc.Dial err: %v", err)
        }
        defer conn.Close()
    
        client := pb.NewStreamServiceClient(conn)
        err = printRoute(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: "gRPC Stream Client: Route", Value: 2019}})
        if err != nil {
                log.Fatalf("printRoute.err: %v", err)
        }
}

func fetchToken() *oauth2.Token{
        return &oauth2.Token{
                AccessToken: "some-secret-token",
        }
}

(5)执行 Server 端和 Client 端的程序,分别输出如下的结果:

// Server
2023/02/26 13:20:20 ====> [Server Stream Interceptor Wrapper]  /proto.StreamService/Route
2023/02/26 13:20:20 ====> [Server Stream interceptor Wrapper] Send a message (Type: %T) at %v pt:{name:"gPRC Stream Client: Route"} 2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 ====> [Server Stream Interceptor Wrapper] Receive a message (Type: %T) at %v  2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2019
2023/02/26 13:20:20 ====> [Server Stream interceptor Wrapper] Send a message (Type: %T) at %v pt:{name:"gPRC Stream Client: Route" value:1} 2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 ====> [Server Stream Interceptor Wrapper] Receive a message (Type: %T) at %v  2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2019
2023/02/26 13:20:20 ====> [Server Stream interceptor Wrapper] Send a message (Type: %T) at %v pt:{name:"gPRC Stream Client: Route" value:2} 2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 ====> [Server Stream Interceptor Wrapper] Receive a message (Type: %T) at %v  2023-02-26T13:20:20+08:00
// Client
2023/02/26 13:20:20 ====> [Client Stream Interceptor]  /proto.StreamService/Route
2023/02/26 13:20:20 ====> [Client Stream interceptor] Send a message (Type: %T) at %v pt:{name:"gRPC Stream Client: Route"  value:2019} 2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 ====> [Client Stream Interceptor] Receive a message (Type: %T) at %v  2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 resp: cqupthao.name: gPRC Stream Client: Route, pt.value: 0
2023/02/26 13:20:20 ====> [Client Stream interceptor] Send a message (Type: %T) at %v pt:{name:"gRPC Stream Client: Route"  value:2019} 2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 ====> [Client Stream Interceptor] Receive a message (Type: %T) at %v  2023-02-26T13:20:20+08:00
2023/02/26 13:20:20 resp: cqupthao.name: gPRC Stream Client: Route, pt.value: 1

  • 参考链接:gRPC 教程

  • 参考链接:gRPC 官网

  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值