在构建 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)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,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)在任意目录下,创建 server 和 client 目录存放服务端和客户端文件,创建 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为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)