gRPC のインターセプター


序章


gRPC アプリケーションを構築する場合、それがクライアント側アプリケーションであってもサーバー側アプリケーションであっても、リモート メソッドの実行前または後に実行する必要がある共通ロジックがいくつかある場合があります。

gRPC は、クライアント側およびサーバー側の gRPC アプリケーションにインターセプターを実装およびインストールするためのシンプルな API を提供します。これは、gRPC のコア拡張メカニズムの 1 つです。一部の使用シナリオ (ロギング、認証、承認、パフォーマンス メトリック、追跡、その他のカスタム要件など) では、インターセプターは各 RPC 呼び出しの実行をインターセプトします。ロギング、認証/認可、メトリクス収集、および RPC 全体で共有できるその他の多くの機能。

gRPC アプリケーションでは、インターセプトされた RPC 呼び出しの種類に応じて、インターセプターは次の 2 つのカテゴリに分類できます。

  • 1 つ目は単項インターセプターで、単項 RPC 呼び出しをインターセプトします。

  • 2 つ目はストリーミング RPC 呼び出しを処理するストリーミング インターセプターで、クライアントとサーバーの両方で通常のインターセプターとストリーミング インターセプターを使用できます。


サーバーインターセプター

クライアントが gRPC サービスのリモート メソッドを呼び出す場合、サーバー側インターセプターを使用することで、リモート メソッドが実行される前に共通ロジックを実行できます。

開発された gRPC サーバー側には、1 つ以上のインターセプターを挿入できます。新しいサーバー側インターセプターを 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 メソッドの呼び出し、後処理の 3 つの段階に分けることができます。

  • 前処理段階。前処理フェーズでは、予期される RPC リモート メソッドを呼び出す前に、ユーザーは受信パラメータをチェックして、RPC コンテキスト、RPC 要求、サーバー側情報などの現在の RPC に関する情報を取得し、それを前処理で変更することもできます。 - 処理フェーズ RPC;

  • コールフェーズ。このフェーズでは、RPC メソッドをトリガーするために gRPC UnaryHandler を呼び出す必要があります。RPC が呼び出された後、後処理フェーズに入ります。これは、RPC 応答が後処理段階を通過することを意味します。

  • ポストステージ。このフェーズでは、返された応答とエラーを必要に応じて処理できます。後処理フェーズが完了したら、メッセージとエラーをインターセプター関数の戻りパラメーターの形式で返す必要があります。ポストプロセッサが必要ない場合は、ハンドラー呼び出し (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 関数を実装します。これら 2 つの関数は、サービスが 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)

クライアント ストリーム インターセプタの実装には、次の前処理ステージとストリーム操作インターセプト ステージが含まれます。

  • 前処理段階。上記の単項インターセプターに似ています。

  • ストリーム操作インターセプトステージ。ストリーム インターセプターは、R​​PC メソッド呼び出しやその後の後処理を実行しませんが、ストリームに対するユーザー操作をインターセプトします。まず、インターセプターは受信ストリーマーを呼び出して 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 をラップする新しい構造を実装する必要があるストリーム ラッパー実装を通じて実現されます。ここでは 2 つのラッピング ストリーム関数 (recvMsg 関数と SendMsg 関数) が実装されており、それぞれクライアントによって送受信されるストリーム メッセージをインターセプトするために使用されます。インターセプターの登録は単項インターセプターの登録と同じであり、grpc.Dial 操作を通じて行われます。


インターセプタプログラムの例


単項インターセプター

(1) 任意のディレクトリに、サーバー ファイルとクライアント ファイルを格納するためのサーバー ディレクトリとクライアント ディレクトリを作成します。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)サーバディレクトリ内のプロジェクト( )を初期化しgo mod init 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)クライアントディレクトリ内のプロジェクト( )を初期化しgo mod init 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
-------------------
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) 任意のディレクトリに、サーバーおよびクライアントファイルを格納するサーバー ディレクトリとクライアント ディレクトリ、証明書ファイルを格納するcertディレクトリ、IDL ファイルを書き込むためのprotointerceptor.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)サーバディレクトリ内のプロジェクト( )を初期化しgo mod init 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)クライアントディレクトリ内のプロジェクト( )を初期化しgo mod init 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
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を例に』(【スリランカ】Kashan Indrasili Danish Kurap)

おすすめ

転載: blog.csdn.net/qq_46457076/article/details/129219721